diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..fe1acf395 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - Device: [e.g. PC] + - OS: [e.g. Windows] + - OS Version (if applicable) [e.g. 11] + - Bombsquad Version [e.g. 1.7.37] + - Plugin Manager Version [e.g. 1.1.10] + +**Smartphone (please complete the following information):** + - Device: [e.g. Mobile] + - OS: [e.g. Android] + - OS Version (if applicable) [e.g. 11] + - Bombsquad Version [e.g. 1.7.37] + - Plugin Manager Version [e.g. 1.1.10] + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..553682dc4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: BombSquad Community Server Discord + url: https://discord.com/invite/ucyaesh + about: Please ask and answer questions here. + - name: BombSquad/Ballistica Official Discord + url: https://ballistica.net/discord + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/plugin-bug-report.md b/.github/ISSUE_TEMPLATE/plugin-bug-report.md new file mode 100644 index 000000000..40c76c04e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/plugin-bug-report.md @@ -0,0 +1,46 @@ +--- +name: Plugin bug report +about: Create a report about a plugin to help us improve +title: '[PLUGIN BUG]' +labels: Plugin Bug +assignees: '' + +--- + +**Plugin name** +The name of the plugin having the bug + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - Device: [e.g. PC] + - OS: [e.g. Windows] + - OS Version (if applicable) [e.g. 11] + - Bombsquad Version [e.g. 1.7.37] + - Plugin Manager Version [e.g. 1.1.10] + - Plugin Version [e.g 2.0.1] + +**Smartphone (please complete the following information):** + - Device: [e.g. Mobile] + - OS: [e.g. Android] + - OS Version (if applicable) [e.g. 11] + - Bombsquad Version [e.g. 1.7.37] + - Plugin Manager Version [e.g. 1.1.10] + - Plugin Version [e.g 2.0.1] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/workflows/MetaData.yml b/.github/workflows/MetaData.yml deleted file mode 100644 index 78d8980ba..000000000 --- a/.github/workflows/MetaData.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: MetaDataCheck - -on: - push: - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Check MetaData - run: | - echo "Analyzing All Py Files" - for pyfile in $(git ls-files '*.py'); do - cat $pyfile | grep "# ba_meta require api 7" > /dev/null || (echo "$pyfile doesn't have the MetaData present in it." && exit 1) - done - echo "All Py Files have MetaData present in them." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..15fe835e5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +name: CI + +# WORD OF CAUTION: +# TO anyone modifying this +# Things will break if you modify this +# without understanding how it works + +# A simple flow of this file: +# Apply AutoPEP8 → Apply Plugin Metadata → CRITICAL COMMIT (format + plugin meta) +# ← ← ← ← ← ↵ +# ↪ Apply Version Metadata → Commit (version meta) → Tests + +on: + push: + branches: + - main + pull_request_target: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install Dependencies + run: | + python -m pip install -U pip + python -m pip install -U pycodestyle==2.12.1 autopep8 + python -m pip install -U -r test/pip_reqs.txt + + - name: Apply AutoPEP8 + run: | + autopep8 --in-place --recursive --max-line-length=100 . + + - name: Apply Plugin Metadata + if: github.event_name == 'pull_request_target' + env: + GH_TOKEN: ${{ github.token }} + run: | + CHANGED_FILES=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --jq '.[].filename') + python test/auto_apply_plugin_metadata.py "$CHANGED_FILES" + + # This is a CRITICAL COMMIT for the next step + # which bases this as the commit to get the sha to store in index.json or plugin.json + - name: Commit Plugin Metadata and AutoPEP8 + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "[ci] apply-plugin-metadata-and-formatting" + branch: ${{ github.head_ref }} + + - name: Apply Version Metadata + run: | + python test/auto_apply_version_metadata.py $(git log --pretty=format:'%h' -n 1) + + - name: Commit Version Metadata + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "[ci] apply-version-metadata" + branch: ${{ github.head_ref }} + + - name: Execute Tests + run: | + python -m unittest discover -v diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml deleted file mode 100644 index 6e4fef062..000000000 --- a/.github/workflows/flake8.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Flake8 - -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 - - name: Execute Flake8 - run: | - flake8 $(git ls-files '*.py') diff --git a/.github/workflows/mod-analyze.yml b/.github/workflows/mod-analyze.yml deleted file mode 100644 index 0d400b249..000000000 --- a/.github/workflows/mod-analyze.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: PluginCheck - -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - sudo apt install jq - - name: Analysing mods - run: | - for jsonfile in $(git ls-files '*.json'); do - echo "Analyzing $jsonfile" - jq -e < $jsonfile > /dev/null || exit 1; - done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..6db088c93 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +name: Create Release + +on: + push: + branches: + - main + paths: + - index.json + +jobs: + build: + name: Create Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: 'Get Previous tag' + uses: oprypin/find-latest-tag@v1.1.2 + with: + repository: ${{ github.repository }} + releases-only: true # We know that all relevant tags have a GitHub release for them. + id: previoustag + + - name: set_variables + run: | + output1=$(python3 test/get_latest.py get_latest_plugman_version) + { + echo "changelog<> "$GITHUB_OUTPUT" + output2=$(python3 test/get_latest.py get_latest_api) + output3=$(python3 test/versioning_tools.py ${{ steps.previoustag.outputs.tag }}) + echo "latestVersion=$output1" >> $GITHUB_OUTPUT + echo "latestAPI=$output2" >> $GITHUB_OUTPUT + echo "shouldRun=$output3" >> $GITHUB_OUTPUT + id: set_variables + + + + - name: Bump version and push tag + if: ${{ steps.set_variables.outputs.shouldRun == '1' }} + id: tag_version + uses: mathieudutour/github-tag-action@v6.2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + custom_tag: | + ${{ steps.set_variables.outputs.latestVersion }} + + - name: Create release + if: ${{ steps.tag_version.outputs.new_tag }} + uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.tag_version.outputs.new_tag }} + name: ${{ steps.tag_version.outputs.new_tag }} (api ${{ steps.set_variables.outputs.latestAPI }}) + artifacts: "plugin_manager.py" + body: | + ## Changelog + ${{ steps.set_variables.outputs.changelog }} + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 536edab45..d13b15ab9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ reference.py +__pycache__ +*.pyc +.history diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..72983d233 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,197 @@ +## Plugin Manager (dd-mm-yyyy) + +### 1.1.4 (09-08-2025) + +- Cleaning up the code. +- UI now works again for other scales if used from dev console. +- Added a new feature to move through plugins. +- Fixed popups getting stuck in background. +- Missing version CHANGELOG no longer causes error + +### 1.1.3 (06-04-2025) + +- Hot Fix + +### 1.1.2 (06-04-2025) + +- Small UI fixes. + +### 1.1.1 (09-02-2025) + +- Added bomb spinner widget for loading animation. + +### 1.1.0 (23-01-2025) + +- Updated to bombsquad api 9. + +### 1.0.23 (13-01-2025) + +- Fixed a bug which wasn't letting older versions run Plugin Manager. + +### 1.0.22 (07-08-2024) + +- Fixed a few Errors and UI Bugs. + +### 1.0.21 (20-05-2024) + +- Fixed an error related with notification of new plugins. + +### 1.0.20 (13-05-2024) + +- Now compatible with BS version 1.7.35+. +- Updated build_number and version attributes to latest. +- FIX: Changelog for all version was shown after refreshing. + +### 1.0.19 (05-05-2024) + +- Fixed an issue where changelogs were not displayed. +- Changed the Authors text color to be seen more easily. + +### 1.0.18 (28-04-2024) + +- Fixed errors which were caused due to no internet connection. + +### 1.0.17 (22-04-2024) + +- Added ChangeLog Window to view latest changes. + +### 1.0.16 (22-04-2024) + +- Fix for error caused when disable button was missing + +### 1.0.15 (22-04-2024) + +- Plugins can now be viewed in A-Z and Z-A order. +- Added 'Installed' category to show Installed plugins. + +### 1.0.14 (21-04-2024) + +- Displaying All Author Names and their Info for plugins. + +### 1.0.13 (20-04-2024) + +- Improvements to the new plugins notification. + +### 1.0.12 (20-04-2024) + +- Limited the "x new plugins are available" screen message to only show maximum 2 plugins. + +### 1.0.11 (20-04-2024) + +- Fixed positions of a few buttons. + +### 1.0.10 (19-04-2024) + +- Fixed up a bug in Refreshing Plugins button. + +### 1.0.9 (19-04-2024) + +- Made the Plugin names look a little cleaner. + +### 1.0.8 (11-04-2024) + +- Avoid making app-mode related calls while `SceneAppMode` isn't set. + +### 1.0.7 (22-02-2024) + +- Fix searching in plugin manager with capital letters. + +### 1.0.6 (26-12-2023) + +- Fixed plugin manager throwing errors on older builds. + +### 1.0.5 (11-12-2023) + +- Fix a typo. + +### 1.0.4 (08-12-2023) + +- Fix a few UI warnings related to 1.7.30. +- Fix a memory leak. + +### 1.0.3 (06-10-2023) + +- Add a compatibility layer for older builds for API deprecation changes that occured in https://github.com/efroemling/ballistica/blob/master/CHANGELOG.md#1727-build-21282-api-8-2023-08-30 + +### 1.0.2 (01-10-2023) + +- Rename deprecated `babase.app.api_version` -> `babase.app.env.api_version`. + +### 1.0.1 (30-06-2023) + +- Allow specifying branch names in custom sources. + +### 1.0.0 (20-06-2023) + +- Migrate plugin manager's source code to API 8. + +### 0.3.5 (16-06-2023) + +- Replace the "Loading..." text with the exception message in case something goes wrong. + +### 0.3.4 (14-05-2023) + +- Optimize new plugin detection mechanism. + +### 0.3.3 (13-05-2023) + +- Print the number and names of the client supported plugins which are newly added to the plugin manager. + +### 0.3.2 (30-04-2023) + +- Fix sometimes same sound would repeat twice when pressing a button. +- Low key attempt to experiment with staging branch by changing current tag in `plugin_manager.py`. +- Assume underscores as spaces when searching for plugins in game. + +### 0.3.1 (04-03-2023) + +- Resize the plugin window to limit the overlapping of plugin description. + +### 0.3.0 (12-02-2023) + +- Displays a tutorial button in the plugin window, whenever there is a supported url present in the plugin data. + +### 0.2.2 (18-01-2023) + +- Auto add new line breaks in long plugin descriptions. +- Fixed an issue where pressing back on the main plugin manager window would play the sound twice. + +### 0.2.1 (17-12-2022) + +- Add Google DNS as a fallback for Jio ISP DNS blocking resolution of raw.githubusercontent.com domain. + +### 0.2.0 (05-12-2022) + +- Removed `on_plugin_manager_prompt` and replaced it with the in-game's plugin settings ui + +### 0.1.10 (05-12-2022) + +- Added Discord and Github textures on buttons + +### 0.1.9 (03-12-2022) + +- Search now filters on author names and plugin description. + +### 0.1.8 (30-11-2022) + +- Use HTTPS for all network requests (now that the Android bug has been resolved as of v1.7.11). + +### 0.1.7 (03-10-2022) + +- Added New Option in settings for Notifying new plugins. +- Added a Discord Button to join Bombsquad's Official Discord server. + + +### 0.1.6 (15-09-2022) + +- Distinguish the settings button with a cyan color (previously was green) in plugin manager window. +- Clean up some internal code. + + +### 0.1.5 (08-09-2022) + +- Plugin files that export classes besides plugin or game, now work. + +### 0.1.4 (05-09-2022) + +- First public release of plugin manager. 🎉 diff --git a/README.md b/README.md index 9b118a2d5..cda60d37a 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,200 @@ -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/bombsquad-community/plugin-manager/Flake8)](https://github.com/bombsquad-community/plugin-manager/actions) +[![CI](https://github.com/bombsquad-community/plugin-manager/actions/workflows/ci.yml/badge.svg)](https://github.com/bombsquad-community/plugin-manager/actions/workflows/ci.yml) -# plugin-manager +**Important:** If you are on BombSquad version less than 1.7.37 , check below. +- for version 1.7.0 to 1.7.19 (which uses API 7), checkout the [api7](https://github.com/bombsquad-community/plugin-manager/tree/api7) branch. +- for version 1.7.20 to 1.7.36 (which uses API 8), checkout the [api8](https://github.com/bombsquad-community/plugin-manager/tree/api8) branch. -A plugin manager for the game - [Bombsquad](https://www.froemling.net/apps/bombsquad). +If you have version 1.7.37 or greater (which uses API 9), proceed with the rest of the README here. +------------------------------- + +# Plugin Manager + +A plugin manager for the game - [BombSquad](https://www.froemling.net/apps/bombsquad). Plugin Manager is a plugin in itself, +which makes further modding of your game more convenient by providing easier access to community created content. + +[![DownloadIcon]][DownloadLink] + +[DownloadIcon]:https://img.shields.io/badge/Download-5555ff?style=for-the-badge&logoColor=white&logo=DocuSign +[DownloadLinkJSdelivr]:https://cdn.jsdelivr.net/gh/bombsquad-community/plugin-manager/plugin_manager.py +[DownloadLink]:https://github.com/bombsquad-community/plugin-manager/releases/latest/download/plugin_manager.py + +![Plugin Manager GIF](https://user-images.githubusercontent.com/106954762/190505304-519c4b91-2461-42b1-be57-655a3fb0cbe8.gif) ## Features -- [x] Fully open-source plugin manager, as well as all the plugins you'll find in this repository. -- [x] Ability to add 3rd party plugin sources (use them at your own risk, since they may not be audited!). -- [x] Only deal with plugins and plugin updates targetting your game's current API version. -- [x] Search installable plugins from this repository, as well as 3rd party sources. -- [x] Setting to enable or disable auto-updates for plugin manager as well plugins. -- [x] Setting to immediately enable installed plugins/minigames without having to restart game. -- [x] Ability to launch a plugin's settings directly from the plugin manager window. -- [x] Check out a plugin's source code before you even install it. +- [x] Completely open-source - both the plugin-manager and all the plugins in this repository. +- [x] Works on all platforms. +- [x] Only deal with plugins and updates targetting your game's current API version. +- [x] Search for plugins. +- [x] Add 3rd party plugin sources (use them at your own risk, since they may not be audited!). +- [x] Enable or disable auto-updates for plugin manager and plugins. +- [x] Immediately enable installed plugins/minigames without having to restart game. +- [x] Launch a plugin's settings directly from the plugin manager window. +- [x] Check out a plugin's source code before installing it. - [ ] Sync installed plugins with workspaces. - [ ] Sort plugins by popularity, downloads, rating or some other metric. ## Installation -- Either download [plugin_manager.py](https://raw.githubusercontent.com/bombsquad-community/plugin-manager/main/plugin_manager.py) - to your mods directory (check it out by going into your game's Settings -> Advanced -> Show Mods Folder) or directly add - it to your workspace! +There are two different ways the plugin manager can be installed: + + +1. From dev console + - Enable "Show Dev Console Button" from advance BombSquad settings + - Paste the following code in dev console + ```py + import urllib.request;import _babase;import os;url="https://github.com/bombsquad-community/plugin-manager/releases/latest/download/plugin_manager.py";plugin_path=os.path.join(_babase.env()["python_directory_user"],"plugin_manager.py");file=urllib.request.urlretrieve(url)[0];fl = open(file,'r');f=open(plugin_path, 'w+');f.write(fl.read());fl.close();f.close();print("SUCCESS") + ``` + - "SUCCESS" will be shown in the console + +2. Another way is to add + [plugin_manager.py](https://raw.githubusercontent.com/bombsquad-community/plugin-manager/main/plugin_manager.py) + to your workspace. However, plugin manager self-updates will fail when installed using this way since the game + will overrwrite the updated plugin manager, with the older version from workspace on the next sync. However, you can + manually apply updates by copying the latest plugin manager's source code again to your workspace when using this method. + +3. [Download plugin_manager.py][DownloadLink] to your mods directory (check it out by going into your game's + Settings -> Advanced -> Show Mods Folder). This is the recommended way (read next method to know why). + If you're on a newer version of Android (11 or above) and not rooted, it probably won't be possible to copy + mods to game's mods folder. In this case, you can connect your Android phone to a computer and push `plugin_manager.py` + [using `adb`](https://www.xda-developers.com/install-adb-windows-macos-linux/): + ```bash + $ adb push plugin_manager.py /sdcard/Android/data/net.froemling.bombsquad/files/mods/plugin_manager.py + ``` + +## Usage + +- If installed correctly, you'll see the plugin manager button in your game's settings. + + + +- That's it, you now have access to a variety of community created content waiting for you to install! ## Contributing ### Submitting a Plugin + - In order for a plugin to get accepted to this official repository, it must target the general game audience and be completely open and readable, not be encoded or encrypted in any form. -- If your plugin doesn't target the general game audience, you can put your plugins in a GitHub repository and then - your plugins can be installed through the custom source option in-game. +- If your plugin doesn't target the general game audience, you can put your plugin(s) in a GitHub repository and then + your plugin(s) can be installed through the custom source option in-game. See [3rd party plugin sources](#3rd-party-plugin-sources) for more information. - New plugins are accepted through a [pull request](../../compare). Add your plugin in the minigames, utilities, or the category directory you feel is the most relevant to the type of plugin you're submitting, [here](plugins). - Then add an entry to the category's JSON metadata file (check out existing entries to know what all fields are required). - Plugin manager will also show and execute the settings icon if your `ba.Plugin` export class has a method named - `on_plugin_manager_prompt`; check out the - [colorscheme](https://github.com/bombsquad-community/plugin-manager/blob/f24f0ca5ded427f6041795021f1af2e6a08b6ce9/plugins/utilities/colorscheme.py#L419-L420) - plugin for an example and it's behaviour when the settings icon is tapped on via the plugin manager in-game. + Then add an entry to the category's JSON metadata file. +- Plugin manager will also show and execute the settings icon if your `ba.Plugin` class has methods `has_settings_ui` and `show_settings_ui`; check out the [colorscheme](https://github.com/bombsquad-community/plugin-manager/blob/eb163cf86014b2a057c4a048dcfa3d5b540b7fe1/plugins/utilities/colorscheme.py#L448-L452) plugin for an example. + +#### Example: + +Let's say you wanna submit this new utility-type plugin named as `sample_plugin.py`: +```python +# ba_meta require api 9 +import babase + +plugman = dict( + plugin_name="sample_plugin", + description="A test plugin for demonstration purposes blah blah.", + external_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", + authors=[ + {"name": "Loup", "email": "loupg450@gmail.com", "discord": "loupgarou_"}, + {"name": "brostos", "email": "", "discord": "brostos"} + ], + version="1.0.0", +) + +# ba_meta export babase.Plugin +class Main(babase.Plugin): + def on_app_running(self): + babase.screenmessage("Hi! I am a sample plugin!") + + def has_settings_ui(self): + return True + + def show_settings_ui(self, source_widget): + babase.screenmessage("You tapped my settings!") +``` + +You'll have to fork this repository and add your `sample_plugin.py` plugin file into the appropriate directory, which for +utility plugin is [plugins/utilities](plugins/utilities). After that, plugin details and version values will automatically be populated through github-actions in [plugins/utilities.json](plugins/utilities.json)(along with formatting your code as per PEP8 style +guide) once you open a pull request. ### Updating a Plugin + - Make a [pull request](../../compare) with whatever changes you'd like to make to an existing plugin, and add a new - version entry in your plugin category's JSON metadata file. + version number in your plugin in the plugman dict. + +#### Example + +Continuing the example from [Submitting a Plugin](#submitting-a-plugin) section, if you also want to add a new screenmessage +to the `sample_plugin.py` plugin after it has been submitted, edit `sample_plugin.py` with whatever changes you'd like: +```diff +diff --git a/plugins/utilities/sample_plugin.py b/plugins/utilities/sample_plugin.py +index ebb7dcc..da2b312 100644 +--- a/plugins/utilities/sample_plugin.py ++++ b/plugins/utilities/sample_plugin.py +@@ -9,7 +9,7 @@ + {"name": "Loup", "email": "loupg450@gmail.com", "discord": "loupgarou_"}, + {"name": "brostos", "email": "", "discord": "brostos"} + ], +- version="1.0.0", ++ version="1.1.0", + ) + + # ba_meta export babase.Plugin +@@ -21,4 +21,4 @@ + return True + + def show_settings_ui(self, source_widget): +- babase.screenmessage("You tapped my settings!") ++ babase.screenmessage("Hey! This is my new screenmessage!") +``` + +That's it! Now you can make a [pull request](../../compare) with the updated `sample_plugin.py` file. ## 3rd Party Plugin Sources + - In case your plugin doesn't sit well with our guidelines or you wouldn't want your plugin to be here for some reason, you can create your own GitHub repository and put all your plugins in there. -- Check out https://github.com/rikkolovescats/sahilp-plugins for an example. You can choose to show up plugins from this - repository in your plugin manager by adding `rikkolovescats/sahilp-plugins` as a custom source through the category - selection popup window in-game. +- Check out [bombsquad-community/sample-plugin-source](https://github.com/bombsquad-community/sample-plugin-source) as an example. + You can choose to show up plugins from this repository in your plugin manager by adding `bombsquad-community/sample-plugin-source` + as a custom source through the category selection popup window in-game. +- Plugin manager will default to picking up plugins from the `main` branch of the custom source repository. You + can specify a different branch by suffixing the source URI with `@branchname`, such as `bombsquad-community/sample-plugin-source@experimental`. + + #### Known 3rd Party Plugin Sources + + If you maintain or know of a 3rd party plugin source, let us know and we'll add it below so people can know about it. It + will also help us to notify the maintainers of any future breaking changes in plugin manager that could affect 3rd party + plugin sources. + + - [rikkolovescats/sahilp-plugins](https://github.com/rikkolovescats/sahilp-plugins) + - [Aeliux/arcane](https://github.com/Aeliux/arcane) + + +## Tests + +Metadata tests are automatically executed whenever a pull request is opened and a commit is pushed. You can also run them +locally by installing test dependencies with: + +```bash +$ pip install -r test/pip_reqs.txt +``` + +and then executing the following in the project's root directory: + +```bash +$ python -m unittest discover -v +``` + +## Shout out! + +If you've been with the community long enough, you may have known about the amazing +[Mrmaxmeier's mod manager](https://github.com/Mrmaxmeier/BombSquad-Community-Mod-Manager), which unfortunately wasn't +maintained and failed to keep up with the game's latest versions and API changes. Well, this is another attempt to +create something similar, with a hope we as a community can continue to keep it up-to-date with the original game. ## License @@ -59,4 +202,6 @@ A plugin manager for the game - [Bombsquad](https://www.froemling.net/apps/bombs - [Plugin manager's source code](plugin_manager.py) is licensed under the MIT license. See [LICENSE](LICENSE) for more information. - Any plugins you submit here are automatically assumed to be licensed under the MIT license, i.e. unless you explicitly - specify a different license while submitting a plugin. + specify a different license in your plugin's source code. See + [this plugin](https://github.com/bombsquad-community/plugin-manager/blob/cba1194c68ce550a71d2f3fadd9e1b8cbac4981c/plugins/utilities/store_event_specials.py#L1-L22) + for an example. diff --git a/index.json b/index.json index 43e3affe2..54310e98e 100644 --- a/index.json +++ b/index.json @@ -1,25 +1,281 @@ { - "plugin_manager_url": "http://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", + "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { - "0.1.1": { - "api_version": 7, - "commit_sha": "123abcd", - "dependencies": [], - "released_on": "someday-2022", - "md5sum": "implementme" - }, - "0.1.0": { - "api_version": 7, - "commit_sha": "a02fe8e", - "dependencies": [], - "released_on": "13-08-2022", - "md5sum": "9038aeffc5836586ec8d61c51ec5ed5d" + "1.1.4": { + "api_version": 9, + "commit_sha": "1a9468d", + "released_on": "09-08-2025", + "md5sum": "638c30b15027156cc6a1803d0ee07399" + }, + "1.1.3": { + "api_version": 9, + "commit_sha": "728b01b", + "released_on": "08-04-2025", + "md5sum": "627d5263a4b2b0708558f1251965d15b" + }, + "1.1.2": { + "api_version": 9, + "commit_sha": "8f484fc", + "released_on": "08-04-2025", + "md5sum": "a6dd38d92c6c850cd1bab866870e48b5" + }, + "1.1.1": { + "api_version": 9, + "commit_sha": "93106f2", + "released_on": "09-02-2025", + "md5sum": "0e5ec54582032a11c044d10cff3d0aa1" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "004ee51", + "released_on": "23-01-2025", + "md5sum": "d58551313ae91498c0b0ba804df1066e" + }, + "1.0.23": { + "api_version": 8, + "commit_sha": "1d6a41f", + "released_on": "13-01-2025", + "md5sum": "74671c01ba4b4042ba416814aa1d6060" + }, + "1.0.22": { + "api_version": 8, + "commit_sha": "719c104", + "released_on": "07-08-2024", + "md5sum": "9c431e0eff32a16070afb7f59a5f0046" + }, + "1.0.21": { + "api_version": 8, + "commit_sha": "b1a3aaa", + "released_on": "20-05-2024", + "md5sum": "e00d6ce92ce4651eceb5ae389ca5c4fb" + }, + "1.0.20": { + "api_version": 8, + "commit_sha": "5ce10ce", + "released_on": "13-05-2024", + "md5sum": "eb49f952e59effaf4529017f4bed4f72" + }, + "1.0.19": { + "api_version": 8, + "commit_sha": "b439c4b", + "released_on": "05-05-2024", + "md5sum": "5cc29a6d7eaafe883e63cd6f0e9153f4" + }, + "1.0.18": { + "api_version": 8, + "commit_sha": "5af504e", + "released_on": "28-04-2024", + "md5sum": "2c231693e686971a7cc4f12d1ffa3fdd" + }, + "1.0.17": { + "api_version": 8, + "commit_sha": "02530f0", + "released_on": "22-04-2024", + "md5sum": "b5f3902046731ae4cc5bc88586203238" + }, + "1.0.16": { + "api_version": 8, + "commit_sha": "0839999", + "released_on": "22-04-2024", + "md5sum": "f9f3f7d3ff9fcbeee7ae01c8dc1a4ab8" + }, + "1.0.15": { + "api_version": 8, + "commit_sha": "fd170eb", + "released_on": "22-04-2024", + "md5sum": "3666dbb82805bdc4885ebf6b105ebf3b" + }, + "1.0.14": { + "api_version": 8, + "commit_sha": "fc5a7f1", + "released_on": "21-04-2024", + "md5sum": "d25f8b80387d532d1d8aa3b84b35b696" + }, + "1.0.13": { + "api_version": 8, + "commit_sha": "2a4cb47", + "released_on": "21-04-2024", + "md5sum": "0d04386a75f692bd80875c6f97b262c5" + }, + "1.0.12": { + "api_version": 8, + "commit_sha": "34bbcba", + "released_on": "20-04-2024", + "md5sum": "6d14d091d02582270c5c93b54d65efd5" + }, + "1.0.11": { + "api_version": 8, + "commit_sha": "4228b3e", + "released_on": "20-04-2024", + "md5sum": "c22d4d2270e17971b6559edac845d0ff" + }, + "1.0.10": { + "api_version": 8, + "commit_sha": "6dd86ef", + "released_on": "19-04-2024", + "md5sum": "06a39b09fc5c0be30a7bf2d65c350310" + }, + "1.0.9": { + "api_version": 8, + "commit_sha": "88834bd", + "released_on": "19-04-2024", + "md5sum": "57157cb438d793370586e1ccc957ad30" + }, + "1.0.8": { + "api_version": 8, + "commit_sha": "85b5727", + "released_on": "11-04-2024", + "md5sum": "74fbf98aa908fc2830f5973101e38b3a" + }, + "1.0.7": { + "api_version": 8, + "commit_sha": "cf55d26", + "released_on": "21-02-2024", + "md5sum": "aa4cbd1f8e1618c30e5cd0bdb02ae962" + }, + "1.0.6": { + "api_version": 8, + "commit_sha": "e5a0e90", + "released_on": "26-12-2023", + "md5sum": "39b379e6243d921313065fcf1f45e908" + }, + "1.0.5": { + "api_version": 8, + "commit_sha": "cd1c858", + "released_on": "11-12-2023", + "md5sum": "bbd7b03d0e16b088c0df842271dbf9cc" + }, + "1.0.4": { + "api_version": 8, + "commit_sha": "94abe80", + "released_on": "08-12-2023", + "md5sum": "eae81ded9f9acee862c529ca6202cc72" + }, + "1.0.3": { + "api_version": 8, + "commit_sha": "2a5ce50", + "released_on": "06-10-2023", + "md5sum": "d8e6267b2eae6fc21efd77bbb47c0a07" + }, + "1.0.2": { + "api_version": 8, + "commit_sha": "818ec65", + "released_on": "01-10-2023", + "md5sum": "9cd1facb888e63ba08b0607a8561991a" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "7dba50e", + "released_on": "29-06-2023", + "md5sum": "099ebc828a13f9a7040486bab4932a55" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "0b55bc2", + "released_on": "20-06-2023", + "md5sum": "98fa2f29c3a2d34b5ff53b7abde7ec64" + }, + "0.3.5": { + "api_version": 7, + "commit_sha": "985e486", + "released_on": "15-06-2023", + "md5sum": "5da137a353b4fa35e4d9926107cf4754" + }, + "0.3.4": { + "api_version": 7, + "commit_sha": "2d74b52", + "released_on": "14-05-2023", + "md5sum": "7ff90318c4c379f7f0039c13ddaeaef0" + }, + "0.3.3": { + "api_version": 7, + "commit_sha": "b48119b", + "released_on": "13-05-2023", + "md5sum": "611c20732eda494d9ad7090b0bc3df54" + }, + "0.3.2": { + "api_version": 7, + "commit_sha": "789f95f", + "released_on": "30-04-2023", + "md5sum": "ff679f8411e426e5e4e92a2f958eec02" + }, + "0.3.1": { + "api_version": 7, + "commit_sha": "0b856ba", + "released_on": "04-03-2023", + "md5sum": "52fdce0f242b1bc52a1cbf2e7d78d230" + }, + "0.3.0": { + "api_version": 7, + "commit_sha": "cb5df36", + "released_on": "12-02-2023", + "md5sum": "d149fedf64b002c97fbb883ae5629d49" + }, + "0.2.2": { + "api_version": 7, + "commit_sha": "2672a5a", + "released_on": "18-01-2023", + "md5sum": "2ef9761e4a02057cd93db3d280427f12" + }, + "0.2.1": { + "api_version": 7, + "commit_sha": "8ac1032", + "released_on": "17-12-2022", + "md5sum": "de90e0b02c450f521c9fef4081239eca" + }, + "0.2.0": { + "api_version": 7, + "commit_sha": "bd9dc14", + "released_on": "05-12-2022", + "md5sum": "3633c9c58037cbad7d2942858270c4ae" + }, + "0.1.10": { + "api_version": 7, + "commit_sha": "e122d0f", + "released_on": "05-12-2022", + "md5sum": "6e1ea3e593f1230e527c583e16d96595" + }, + "0.1.9": { + "api_version": 7, + "commit_sha": "09b6832", + "released_on": "03-12-2022", + "md5sum": "2582a1b8448b84d17b739caeeaad51e1" + }, + "0.1.8": { + "api_version": 7, + "commit_sha": "bbce3bd", + "released_on": "30-11-2022", + "md5sum": "0218c7d6b08896f18479467fdd45aa27" + }, + "0.1.7": { + "api_version": 7, + "commit_sha": "2fe966c", + "released_on": "04-10-2022", + "md5sum": "78d7a6b3a62f8920d0da0acab20b419a" + }, + "0.1.6": { + "api_version": 7, + "commit_sha": "2a7ad8e", + "released_on": "15-09-2022", + "md5sum": "56950f6455606f7705eebd3e9a38c78f" + }, + "0.1.5": { + "api_version": 7, + "commit_sha": "6998ded", + "released_on": "08-09-2022", + "md5sum": "d88925069a3ee73c2ea5141539b0a286" + }, + "0.1.4": { + "api_version": 7, + "commit_sha": "7c4504c", + "released_on": "05-09-2022", + "md5sum": "52e03d303e55738d3653f86fb5abe2de" } }, "categories": [ - "http://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/utilities.json", - "http://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/minigames.json", - "http://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/maps.json" + "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/utilities.json", + "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/minigames.json", + "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/maps.json" ], - "external_source_url": "http://github.com/{repository}/{content_type}/{tag}/category.json" -} + "external_source_url": "https://github.com/{repository}/{content_type}/{tag}/category.json" +} \ No newline at end of file diff --git a/plugin_manager.py b/plugin_manager.py index d057ba584..d2558774a 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -1,56 +1,127 @@ -# ba_meta require api 7 -import ba -import _ba -from bastd.ui import popup +# ba_meta require api 9 +import babase +import _babase +import _bauiv1 +import _bascenev1 +import bauiv1 as bui +from bauiv1lib import popup, confirm +from babase._meta import EXPORT_CLASS_NAME_SHORTCUTS +from bauiv1lib.settings.allsettings import AllSettingsWindow import urllib.request +import http.client +import socket import json +import ssl + +import re import os import sys +import copy import asyncio -import re import pathlib -import contextlib import hashlib -import copy - -from typing import Union, Optional +import contextlib -_env = _ba.env() -_uiscale = ba.app.ui.uiscale +from typing import cast, override +from datetime import datetime +# Modules used for overriding AllSettingsWindow +import logging -PLUGIN_MANAGER_VERSION = "0.1.1" -REPOSITORY_URL = "http://github.com/bombsquad-community/plugin-manager" +PLUGIN_MANAGER_VERSION = "1.1.4" +REPOSITORY_URL = "https://github.com/bombsquad-community/plugin-manager" +# Current tag can be changed to "staging" or any other branch in +# plugin manager repo for testing purpose. CURRENT_TAG = "main" -# XXX: Using https with `ba.open_url` seems to trigger a pop-up dialog box on -# Android currently (v1.7.6) and won't open the actual URL in a web-browser. -# Let's fallback to http for now until this gets resolved. + +_env = _babase.env() +_app_api_version = babase.app.env.api_version + INDEX_META = "{repository_url}/{content_type}/{tag}/index.json" +CHANGELOG_META = "{repository_url}/{content_type}/{tag}/CHANGELOG.md" HEADERS = { - "User-Agent": _env["user_agent_string"], + "User-Agent": _env["legacy_user_agent_string"], } PLUGIN_DIRECTORY = _env["python_directory_user"] +loop = babase._asyncio._asyncio_event_loop + +open_popups = [] + + +def _add_popup(popup): open_popups.append(popup) + + +def _remove_popup(popup): + try: + open_popups.remove(popup) + except ValueError: + pass + + +def _uiscale(): return bui.app.ui_v1.uiscale +def _regexp_friendly_class_name_shortcut(string): return string.replace(".", "\\.") + + REGEXP = { "plugin_api_version": re.compile(b"(?<=ba_meta require api )(.*)"), - "plugin_entry_points": re.compile(b"(ba_meta export plugin\n+class )(.*)\\("), - "minigames": re.compile(b"(ba_meta export game\n+class )(.*)\\("), + "plugin_entry_points": re.compile( + bytes( + "(ba_meta export (plugin|{})\n+class )(.*)\\(".format( + _regexp_friendly_class_name_shortcut(EXPORT_CLASS_NAME_SHORTCUTS["plugin"]), + ), + "utf-8" + ), + ), + "minigames": re.compile( + bytes( + "(ba_meta export ({})\n+class )(.*)\\(".format( + _regexp_friendly_class_name_shortcut("bascenev1.GameActivity"), + ), + "utf-8" + ), + ), } +DISCORD_URL = "https://ballistica.net/discord" + _CACHE = {} +class MD5CheckSumFailed(Exception): + pass + + +class PluginNotInstalled(Exception): + pass + + +class CategoryDoesNotExist(Exception): + pass + + +class NoCompatibleVersion(Exception): + pass + + +class PluginSourceNetworkError(Exception): + pass + + +class CategoryMetadataParseError(Exception): + pass + + def send_network_request(request): return urllib.request.urlopen(request) async def async_send_network_request(request): - loop = asyncio.get_event_loop() response = await loop.run_in_executor(None, send_network_request, request) return response -def stream_network_response_to_file(request, file): +def stream_network_response_to_file(request, file, md5sum=None, retries=3): response = urllib.request.urlopen(request) chunk_size = 16 * 1024 content = b"" @@ -61,17 +132,10 @@ def stream_network_response_to_file(request, file): break fout.write(chunk) content += chunk - return content - - -async def async_stream_network_response_to_file(request, file, md5sum=None, retries=3): - loop = asyncio.get_event_loop() - content = await loop.run_in_executor(None, stream_network_response_to_file, request, file) if md5sum and hashlib.md5(content).hexdigest() != md5sum: if retries <= 0: - # TODO: Raise a more fitting exception. - raise TypeError("md5sum match failed") - return await async_stream_network_response_to_file( + raise MD5CheckSumFailed("MD5 checksum match failed.") + return stream_network_response_to_file( request, file, md5sum=md5sum, @@ -80,8 +144,17 @@ async def async_stream_network_response_to_file(request, file, md5sum=None, retr return content -def play_sound(): - ba.playsound(ba.getsound('swish')) +async def async_stream_network_response_to_file(request, file, md5sum=None, retries=3): + + content = await loop.run_in_executor( + None, + stream_network_response_to_file, + request, + file, + md5sum, + retries, + ) + return content def partial_format(string_template, **kwargs): @@ -90,6 +163,103 @@ def partial_format(string_template, **kwargs): return string_template +class DNSBlockWorkaround: + """ + Some ISPs put a DNS block on domains that are needed for plugin manager to + work properly. This class stores methods to workaround such blocks by adding + dns.google as a fallback. + + Such as Jio, a pretty popular ISP in India has a DNS block on + raw.githubusercontent.com (sigh..). + + References: + * https://github.com/orgs/community/discussions/42655 + + Usage: + ----- + >>> import urllib.request + >>> import http.client + >>> import socket + >>> import ssl + >>> import json + >>> DNSBlockWorkaround.apply() + >>> response = urllib.request.urlopen("https://dnsblockeddomain.com/path/to/resource/") + """ + + _google_dns_cache = {} + + def apply(): + opener = urllib.request.build_opener( + DNSBlockWorkaround._HTTPHandler, + DNSBlockWorkaround._HTTPSHandler, + ) + urllib.request.install_opener(opener) + + def _resolve_using_google_dns(hostname): + response = urllib.request.urlopen(f"https://dns.google/resolve?name={hostname}") + response = response.read() + response = json.loads(response) + resolved_host = response["Answer"][0]["data"] + return resolved_host + + def _resolve_using_system_dns(hostname): + resolved_host = socket.gethostbyname(hostname) + return resolved_host + + def _resolve_with_workaround(hostname): + resolved_host_from_cache = DNSBlockWorkaround._google_dns_cache.get(hostname) + if resolved_host_from_cache: + return resolved_host_from_cache + + resolved_host_by_system_dns = DNSBlockWorkaround._resolve_using_system_dns(hostname) + + if DNSBlockWorkaround._is_blocked(hostname, resolved_host_by_system_dns): + resolved_host = DNSBlockWorkaround._resolve_using_google_dns(hostname) + DNSBlockWorkaround._google_dns_cache[hostname] = resolved_host + else: + resolved_host = resolved_host_by_system_dns + + return resolved_host + + def _is_blocked(hostname, address): + is_blocked = False + if hostname == "raw.githubusercontent.com": + # Jio's DNS server may be blocking it. + is_blocked = address.startswith("49.44.") + + return is_blocked + + class _HTTPConnection(http.client.HTTPConnection): + def connect(self): + host = DNSBlockWorkaround._resolve_with_workaround(self.host) + self.sock = socket.create_connection( + (host, self.port), + self.timeout, + ) + + class _HTTPSConnection(http.client.HTTPSConnection): + def connect(self): + host = DNSBlockWorkaround._resolve_with_workaround(self.host) + sock = socket.create_connection( + (host, self.port), + self.timeout, + ) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.verify_mode = ssl.CERT_REQUIRED + context.check_hostname = True + context.load_default_certs() + sock = context.wrap_socket(sock, server_hostname=self.host) + self.sock = sock + + class _HTTPHandler(urllib.request.HTTPHandler): + def http_open(self, req): + return self.do_open(DNSBlockWorkaround._HTTPConnection, req) + + class _HTTPSHandler(urllib.request.HTTPSHandler): + def https_open(self, req): + return self.do_open(DNSBlockWorkaround._HTTPSConnection, req) + + class StartupTasks: def __init__(self): self.plugin_manager = PluginManager() @@ -97,12 +267,11 @@ def __init__(self): def setup_config(self): # is_config_updated = False existing_plugin_manager_config = copy.deepcopy( - ba.app.config.get("Community Plugin Manager")) + babase.app.config.get("Community Plugin Manager")) - plugin_manager_config = ba.app.config.setdefault("Community Plugin Manager", {}) + plugin_manager_config = babase.app.config.setdefault("Community Plugin Manager", {}) plugin_manager_config.setdefault("Custom Sources", []) installed_plugins = plugin_manager_config.setdefault("Installed Plugins", {}) - for plugin_name in tuple(installed_plugins.keys()): plugin = PluginLocal(plugin_name) if not plugin.is_installed: @@ -113,6 +282,7 @@ def setup_config(self): "Auto Update Plugin Manager": True, "Auto Update Plugins": True, "Auto Enable Plugins After Installation": True, + "Notify New Plugins": True } settings = plugin_manager_config.setdefault("Settings", {}) @@ -123,53 +293,116 @@ def setup_config(self): plugin_manager_config["Settings"] = current_settings if plugin_manager_config != existing_plugin_manager_config: - ba.app.config.commit() + babase.app.config.commit() async def update_plugin_manager(self): - if not ba.app.config["Community Plugin Manager"]["Settings"]["Auto Update Plugin Manager"]: + if not babase.app.config["Community Plugin Manager"]["Settings"]["Auto Update Plugin Manager"]: return update_details = await self.plugin_manager.get_update_details() if update_details: to_version, commit_sha = update_details - ba.screenmessage(f"Plugin Manager is being updated to version v{to_version}.") - await self.plugin_manager.update(to_version, commit_sha) - ba.screenmessage("Update successful. Restart game to reload changes.", - color=(0, 1, 0)) + bui.screenmessage(f"Plugin Manager is being updated to v{to_version}") + try: + await self.plugin_manager.update(to_version, commit_sha) + except MD5CheckSumFailed: + bui.getsound('error').play() + else: + bui.screenmessage("Update successful. Restart game to reload changes.", + color=(0, 1, 0)) + bui.getsound('shieldUp').play() async def update_plugins(self): - if not ba.app.config["Community Plugin Manager"]["Settings"]["Auto Update Plugins"]: + if not babase.app.config["Community Plugin Manager"]["Settings"]["Auto Update Plugins"]: return await self.plugin_manager.setup_index() all_plugins = await self.plugin_manager.categories["All"].get_plugins() plugins_to_update = [] for plugin in all_plugins: - if await plugin.get_local().is_enabled() and plugin.has_update(): + if plugin.is_installed and await plugin.get_local().is_enabled() and plugin.has_update(): plugins_to_update.append(plugin.update()) await asyncio.gather(*plugins_to_update) + @staticmethod + def _is_new_supported_plugin(plugin): + is_an_update = len(plugin.versions) > 1 + if is_an_update: + return False + try: + plugin.latest_compatible_version + except NoCompatibleVersion: + return False + else: + return True + + async def notify_new_plugins(self): + if not babase.app.config["Community Plugin Manager"]["Settings"]["Notify New Plugins"]: + return + show_max_names = 2 + await self.plugin_manager.setup_index() + new_num_of_plugins = len(await self.plugin_manager.categories["All"].get_plugins()) + try: + existing_num_of_plugins = babase.app.config["Community Plugin Manager"]["Existing Number of Plugins"] + except KeyError: + babase.app.config["Community Plugin Manager"]["Existing Number of Plugins"] = new_num_of_plugins + babase.app.config.commit() + return + + def title_it(plug): + plug = str(plug).replace('_', ' ').title() + return plug + if existing_num_of_plugins < new_num_of_plugins: + new_plugin_count = new_num_of_plugins - existing_num_of_plugins + all_plugins = await self.plugin_manager.categories["All"].get_plugins() + new_supported_plugins = list(filter(self._is_new_supported_plugin, all_plugins)) + new_supported_plugins.sort( + key=lambda plugin: plugin.latest_compatible_version.released_on_date, + reverse=True, + ) + new_supported_plugins = new_supported_plugins[:new_plugin_count] + new_supported_plugins_count = len(new_supported_plugins) + if new_supported_plugins_count > 0: + new_supported_plugins = ", ".join(map(title_it, (new_supported_plugins + if new_supported_plugins_count <= show_max_names else + new_supported_plugins[0:show_max_names]) + )) + if new_supported_plugins_count == 1: + notification_text = f"{new_supported_plugins_count} new plugin ({new_supported_plugins}) is available!" + else: + notification_text = new_supported_plugins + \ + ('' if new_supported_plugins_count <= show_max_names else ' and +' + + str(new_supported_plugins_count-show_max_names)) + " new plugins are available" + bui.screenmessage(notification_text, color=(0, 1, 0)) + + if existing_num_of_plugins != new_num_of_plugins: + babase.app.config["Community Plugin Manager"]["Existing Number of Plugins"] = new_num_of_plugins + babase.app.config.commit() + async def execute(self): self.setup_config() try: await asyncio.gather( self.update_plugin_manager(), self.update_plugins(), + self.notify_new_plugins(), ) except urllib.error.URLError: pass class Category: - def __init__(self, meta_url, is_3rd_party=False): + def __init__(self, meta_url, tag=CURRENT_TAG): self.meta_url = meta_url - self.is_3rd_party = is_3rd_party + self.tag = tag self.request_headers = HEADERS self._metadata = _CACHE.get("categories", {}).get(meta_url, {}).get("metadata") self._plugins = _CACHE.get("categories", {}).get(meta_url, {}).get("plugins") async def fetch_metadata(self): if self._metadata is None: + # Let's keep depending on the "main" branch for 3rd party sources + # even if we're using a different branch of plugin manager's repository. request = urllib.request.Request( - self.meta_url.format(content_type="raw", tag=CURRENT_TAG), + self.meta_url.format(content_type="raw", tag=self.tag), headers=self.request_headers, ) response = await async_send_network_request(request) @@ -177,8 +410,13 @@ async def fetch_metadata(self): self.set_category_global_cache("metadata", self._metadata) return self - async def is_valid(self): - await self.fetch_metadata() + async def validate(self): + try: + await self.fetch_metadata() + except urllib.error.HTTPError as e: + raise PluginSourceNetworkError(str(e)) + except json.decoder.JSONDecodeError as e: + raise CategoryMetadataParseError(f"Failed to parse JSON: {str(e)}") try: await asyncio.gather( self.get_name(), @@ -187,7 +425,7 @@ async def is_valid(self): self.get_plugins(), ) except KeyError: - return False + raise CategoryMetadataParseError(f"Failed to parse JSON; missing required fields.") else: return True @@ -210,7 +448,7 @@ async def get_plugins(self): Plugin( plugin_info, f"{await self.get_plugins_base_url()}/{plugin_info[0]}.py", - is_3rd_party=self.is_3rd_party, + tag=self.tag, ) for plugin_info in self._metadata["plugins"].items() ]) @@ -240,8 +478,8 @@ async def refresh(self): await self.get_plugins() def save(self): - ba.app.config["Community Plugin Manager"]["Custom Sources"].append(self.meta_url) - ba.app.config.commit() + babase.app.config["Community Plugin Manager"]["Custom Sources"].append(self.meta_url) + babase.app.config.commit() class CategoryAll(Category): @@ -274,11 +512,11 @@ def is_installed(self): @property def is_installed_via_plugin_manager(self): - return self.name in ba.app.config["Community Plugin Manager"]["Installed Plugins"] + return self.name in babase.app.config["Community Plugin Manager"]["Installed Plugins"] def initialize(self): - if self.name not in ba.app.config["Community Plugin Manager"]["Installed Plugins"]: - ba.app.config["Community Plugin Manager"]["Installed Plugins"][self.name] = {} + if self.name not in babase.app.config["Community Plugin Manager"]["Installed Plugins"]: + babase.app.config["Community Plugin Manager"]["Installed Plugins"][self.name] = {} return self async def uninstall(self): @@ -289,7 +527,7 @@ async def uninstall(self): except FileNotFoundError: pass try: - del ba.app.config["Community Plugin Manager"]["Installed Plugins"][self.name] + del babase.app.config["Community Plugin Manager"]["Installed Plugins"][self.name] except KeyError: pass else: @@ -298,7 +536,7 @@ async def uninstall(self): @property def version(self): try: - version = (ba.app.config["Community Plugin Manager"] + version = (babase.app.config["Community Plugin Manager"] ["Installed Plugins"][self.name]["version"]) except KeyError: version = None @@ -313,22 +551,20 @@ def _set_content(self, content): fout.write(content) def has_settings(self): - for plugin_entry_point, plugin_class in ba.app.plugins.active_plugins.items(): + for plugin_entry_point, plugin_spec in bui.app.plugins.plugin_specs.items(): if plugin_entry_point.startswith(self._entry_point_initials): - return hasattr(plugin_class, "on_plugin_manager_prompt") - return False + return plugin_spec.plugin.has_settings_ui() - def launch_settings(self): - for plugin_entry_point, plugin_class in ba.app.plugins.active_plugins.items(): + def launch_settings(self, source_widget): + for plugin_entry_point, plugin_spec in bui.app.plugins.plugin_specs.items(): if plugin_entry_point.startswith(self._entry_point_initials): - return plugin_class.on_plugin_manager_prompt() + return plugin_spec.plugin.show_settings_ui(source_widget) async def get_content(self): if self._content is None: if not self.is_installed: - # TODO: Raise a more fitting exception. - raise TypeError("Plugin is not available locally.") - loop = asyncio.get_event_loop() + raise PluginNotInstalled("Plugin is not available locally.") + self._content = await loop.run_in_executor(None, self._get_content) return self._content @@ -342,8 +578,8 @@ async def get_entry_points(self): if not self._entry_points: content = await self.get_content() groups = REGEXP["plugin_entry_points"].findall(content) - # Actual entry points are stored in the first index inside the matching groups. - entry_points = tuple(f"{self.name}.{group[1].decode('utf-8')}" for group in groups) + # Actual entry points are stored in the last index inside the matching groups. + entry_points = tuple(f"{self.name}.{group[-1].decode('utf-8')}" for group in groups) self._entry_points = entry_points return self._entry_points @@ -353,132 +589,115 @@ async def has_minigames(self): self._has_minigames = REGEXP["minigames"].search(content) is not None return self._has_minigames + async def has_plugins(self): + entry_points = await self.get_entry_points() + return len(entry_points) > 0 + def load_minigames(self): - scanner = ba._meta.DirectoryScan(paths="") + scanner = babase._meta.DirectoryScan(paths="") directory, module = self.install_path.rsplit(os.path.sep, 1) scanner._scan_module( pathlib.Path(directory), pathlib.Path(module), ) - scanned_results = set(ba.app.meta.scanresults.exports["ba.GameActivity"]) - for game in scanner.results.exports["ba.GameActivity"]: + scanned_results = set(babase.app.meta.scanresults.exports["bascenev1.GameActivity"]) + for game in scanner.results.exports["bascenev1.GameActivity"]: if game not in scanned_results: - ba.screenmessage(f"{game} minigame loaded", color=(0, 1, 0)) - ba.app.meta.scanresults.exports["ba.GameActivity"].append(game) + bui.screenmessage(f"{game} minigame loaded") + babase.app.meta.scanresults.exports["bascenev1.GameActivity"].append(game) def unload_minigames(self): - scanner = ba._meta.DirectoryScan(paths="") + scanner = babase._meta.DirectoryScan(paths="") directory, module = self.install_path.rsplit(os.path.sep, 1) scanner._scan_module( pathlib.Path(directory), pathlib.Path(module), ) new_scanned_results_games = [] - for game in ba.app.meta.scanresults.exports["ba.GameActivity"]: - if game in scanner.results.exports["ba.GameActivity"]: - ba.screenmessage(f"{game} minigame unloaded", color=(0, 1, 0)) + for game in babase.app.meta.scanresults.exports["bascenev1.GameActivity"]: + if game in scanner.results.exports["bascenev1.GameActivity"]: + bui.screenmessage(f"{game} minigame unloaded") else: new_scanned_results_games.append(game) - ba.app.meta.scanresults.exports["ba.GameActivity"] = new_scanned_results_games + babase.app.meta.scanresults.exports["bascenev1.GameActivity"] = new_scanned_results_games async def is_enabled(self): """ Return True even if a single entry point is enabled or contains minigames. """ - for entry_point, plugin_info in ba.app.config["Plugins"].items(): + if not await self.has_plugins(): + return True + for entry_point, plugin_info in babase.app.config["Plugins"].items(): if entry_point.startswith(self._entry_point_initials) and plugin_info["enabled"]: return True - # XXX: The below logic is more accurate but less efficient, since it actually - # reads the local plugin file and parses entry points from it. - # for entry_point in await self.get_entry_points(): - # if ba.app.config["Plugins"][entry_point]["enabled"]: - # return True - return await self.has_minigames() - - # XXX: Commenting this out for now, since `enable` and `disable` currently have their - # own separate logic. - # async def _set_status(self, to_enable=True): - # for entry_point in await self.get_entry_points: - # if entry_point not in ba.app.config["Plugins"]: - # ba.app.config["Plugins"][entry_point] = {} - # ba.app.config["Plugins"][entry_point]["enabled"] = to_enable + return False async def enable(self): for entry_point in await self.get_entry_points(): - if entry_point not in ba.app.config["Plugins"]: - ba.app.config["Plugins"][entry_point] = {} - ba.app.config["Plugins"][entry_point]["enabled"] = True - if entry_point not in ba.app.plugins.active_plugins: + if entry_point not in babase.app.config["Plugins"]: + babase.app.config["Plugins"][entry_point] = {} + babase.app.config["Plugins"][entry_point]["enabled"] = True + plugin_spec = bui.app.plugins.plugin_specs.get(entry_point) + if plugin_spec not in bui.app.plugins.active_plugins: self.load_plugin(entry_point) - ba.screenmessage(f"{entry_point} loaded", color=(0, 1, 0)) + bui.screenmessage(f"{entry_point} loaded") if await self.has_minigames(): self.load_minigames() - # await self._set_status(to_enable=True) self.save() def load_plugin(self, entry_point): - plugin_class = ba._general.getclass(entry_point, ba.Plugin) - loaded_plugin_class = plugin_class() - loaded_plugin_class.on_app_running() - ba.app.plugins.active_plugins[entry_point] = loaded_plugin_class + plugin_class = babase._general.getclass(entry_point, babase.Plugin) + loaded_plugin_instance = plugin_class() + loaded_plugin_instance.on_app_running() + + plugin_spec = babase.PluginSpec(class_path=entry_point, loadable=True) + plugin_spec.enabled = True + plugin_spec.plugin = loaded_plugin_instance + bui.app.plugins.plugin_specs[entry_point] = plugin_spec + bui.app.plugins.active_plugins.append(plugin_spec.plugin) def disable(self): - for entry_point, plugin_info in ba.app.config["Plugins"].items(): + for entry_point, plugin_info in babase.app.config["Plugins"].items(): if entry_point.startswith(self._entry_point_initials): - # if plugin_info["enabled"]: plugin_info["enabled"] = False - # XXX: The below logic is more accurate but less efficient, since it actually - # reads the local plugin file and parses entry points from it. - # await self._set_status(to_enable=False) self.save() def set_version(self, version): - app = ba.app + app = babase.app app.config["Community Plugin Manager"]["Installed Plugins"][self.name]["version"] = version return self - # def set_entry_points(self): - # if not "entry_points" in ba.app.config["Community Plugin Manager"] - # ["Installed Plugins"][self.name]: - # ba.app.config["Community Plugin Manager"]["Installed Plugins"] - # [self.name]["entry_points"] = [] - # for entry_point in await self.get_entry_points(): - # ba.app.config["Community Plugin Manager"]["Installed Plugins"][self.name] - # ["entry_points"].append(entry_point) - async def set_content(self, content): if not self._content: - loop = asyncio.get_event_loop() + await loop.run_in_executor(None, self._set_content, content) self._content = content return self - async def set_content_from_network_response(self, request, md5sum=None): + async def set_content_from_network_response(self, request, md5sum=None, retries=3): if not self._content: self._content = await async_stream_network_response_to_file( request, self.install_path, md5sum=md5sum, + retries=retries, ) return self._content def save(self): - ba.app.config.commit() + babase.app.config.commit() return self class PluginVersion: - def __init__(self, plugin, version, tag=None): + def __init__(self, plugin, version, tag=CURRENT_TAG): self.number, info = version self.plugin = plugin self.api_version = info["api_version"] + self.released_on = info["released_on"] self.commit_sha = info["commit_sha"] - self.dependencies = info["dependencies"] self.md5sum = info["md5sum"] - if tag is None: - tag = self.commit_sha - self.download_url = self.plugin.url.format(content_type="raw", tag=tag) self.view_url = self.plugin.url.format(content_type="blob", tag=tag) @@ -489,37 +708,47 @@ def __eq__(self, plugin_version): def __repr__(self): return f"" + @property + def released_on_date(self): + return datetime.strptime(self.released_on, "%d-%m-%Y") + async def _download(self, retries=3): local_plugin = self.plugin.create_local() - await local_plugin.set_content_from_network_response(self.download_url, md5sum=self.md5sum) + await local_plugin.set_content_from_network_response( + self.download_url, + md5sum=self.md5sum, + retries=retries, + ) local_plugin.set_version(self.number) local_plugin.save() return local_plugin - async def install(self): - local_plugin = await self._download() - ba.screenmessage(f"{self.plugin.name} installed", color=(0, 1, 0)) - check = ba.app.config["Community Plugin Manager"]["Settings"] - if check["Auto Enable Plugins After Installation"]: - await local_plugin.enable() + async def install(self, suppress_screenmessage=False): + try: + local_plugin = await self._download() + except MD5CheckSumFailed: + if not suppress_screenmessage: + bui.screenmessage( + f"{self.plugin.name} failed MD5 checksum during installation", color=(1, 0, 0)) + return False + else: + if not suppress_screenmessage: + bui.screenmessage(f"{self.plugin.name} installed", color=(0, 1, 0)) + check = babase.app.config["Community Plugin Manager"]["Settings"] + if check["Auto Enable Plugins After Installation"]: + await local_plugin.enable() + return True class Plugin: - def __init__(self, plugin, url, is_3rd_party=False): + def __init__(self, plugin, url, tag=CURRENT_TAG): """ Initialize a plugin from network repository. """ self.name, self.info = plugin - self.is_3rd_party = is_3rd_party self.install_path = os.path.join(PLUGIN_DIRECTORY, f"{self.name}.py") - # if is_3rd_party: - # tag = CURRENT_TAG - # else: - # tag = CURRENT_TAG - tag = CURRENT_TAG self.url = url - self.download_url = url.format(content_type="raw", tag=tag) - self.view_url = url.format(content_type="blob", tag=tag) + self.tag = tag self._local_plugin = None self._versions = None @@ -529,6 +758,17 @@ def __init__(self, plugin, url, is_3rd_party=False): def __repr__(self): return f"" + def __str__(self): + return self.name + + @property + def view_url(self): + if self.latest_compatible_version == self.latest_version: + tag = CURRENT_TAG + else: + tag = self.latest_compatible_version.commit_sha + return self.url.format(content_type="blob", tag=tag) + @property def is_installed(self): return os.path.isfile(self.install_path) @@ -540,6 +780,7 @@ def versions(self): PluginVersion( self, version, + tag=self.tag, ) for version in self.info["versions"].items() ] return self._versions @@ -550,7 +791,7 @@ def latest_version(self): self._latest_version = PluginVersion( self, tuple(self.info["versions"].items())[0], - tag=CURRENT_TAG, + tag=self.tag, ) return self._latest_version @@ -558,18 +799,23 @@ def latest_version(self): def latest_compatible_version(self): if self._latest_compatible_version is None: for number, info in self.info["versions"].items(): - if info["api_version"] == ba.app.api_version: + if info["api_version"] == _app_api_version: self._latest_compatible_version = PluginVersion( self, (number, info), - CURRENT_TAG if self.latest_version.number == number else None + tag=self.tag if self.latest_version.number == number else info["commit_sha"] ) break + if self._latest_compatible_version is None: + raise NoCompatibleVersion( + f"{self.name} has no version compatible with API {_app_api_version}." + ) return self._latest_compatible_version def get_local(self): if not self.is_installed: - raise ValueError(f"{self.name} is not installed") + raise PluginNotInstalled( + f"{self.name} needs to be installed to get its local plugin.") if self._local_plugin is None: self._local_plugin = PluginLocal(self.name) return self._local_plugin @@ -582,240 +828,37 @@ def create_local(self): async def uninstall(self): await self.get_local().uninstall() - ba.screenmessage(f"{self.name} uninstalled", color=(0, 1, 0)) + bui.screenmessage(f"{self.name} uninstalled", color=(0.9, 1, 0)) def has_update(self): - return self.get_local().version != self.latest_compatible_version.number - - async def update(self): - await self.latest_compatible_version.install() - ba.screenmessage(f"{self.name} updated to {self.latest_compatible_version.number}", - color=(0, 1, 0)) - - -class PluginWindow(popup.PopupWindow): - def __init__(self, plugin, origin_widget, button_callback=lambda: None): - self.plugin = plugin - self.button_callback = button_callback - self.scale_origin = origin_widget.get_screen_space_center() - loop = asyncio.get_event_loop() - loop.create_task(self.draw_ui()) - - async def draw_ui(self): - # print(ba.app.plugins.active_plugins) - play_sound() - b_text_color = (0.75, 0.7, 0.8) - s = 1.1 if _uiscale is ba.UIScale.SMALL else 1.27 if ba.UIScale.MEDIUM else 1.57 - width = 360 * s - height = 100 + 100 * s - color = (1, 1, 1) - text_scale = 0.7 * s - self._transition_out = 'out_scale' - transition = 'in_scale' - - self._root_widget = ba.containerwidget(size=(width, height), - # parent=_ba.get_special_widget( - # 'overlay_stack'), - on_outside_click_call=self._ok, - transition=transition, - scale=(2.1 if _uiscale is ba.UIScale.SMALL else 1.5 - if _uiscale is ba.UIScale.MEDIUM else 1.0), - scale_origin_stack_offset=self.scale_origin) - - pos = height * 0.8 - plugin_title = f"{self.plugin.name} (v{self.plugin.latest_compatible_version.number})" - ba.textwidget(parent=self._root_widget, - position=(width * 0.49, pos), size=(0, 0), - h_align='center', v_align='center', text=plugin_title, - scale=text_scale * 1.25, color=color, - maxwidth=width * 0.9) - pos -= 25 - # author = - ba.textwidget(parent=self._root_widget, - position=(width * 0.49, pos), - size=(0, 0), - h_align='center', - v_align='center', - text='by ' + self.plugin.info["authors"][0]["name"], - scale=text_scale * 0.8, - color=color, maxwidth=width * 0.9) - pos -= 35 - # status = ba.textwidget(parent=self._root_widget, - # position=(width * 0.49, pos), size=(0, 0), - # h_align='center', v_align='center', - # text=status_text, scale=text_scale * 0.8, - # color=color, maxwidth=width * 0.9) - pos -= 25 - # info = - ba.textwidget(parent=self._root_widget, - position=(width * 0.49, pos), size=(0, 0), - h_align='center', v_align='center', - text=self.plugin.info["description"], - scale=text_scale * 0.6, color=color, - maxwidth=width * 0.95) - b1_color = (0.6, 0.53, 0.63) - b2_color = (0.8, 0.15, 0.35) - b3_color = (0.2, 0.8, 0.3) - pos = height * 0.1 - button_size = (80 * s, 40 * s) - - to_draw_button1 = True - to_draw_button4 = False - if self.plugin.is_installed: - self.local_plugin = self.plugin.get_local() - if await self.local_plugin.has_minigames(): - to_draw_button1 = False - else: - if await self.local_plugin.is_enabled(): - button1_label = "Disable" - button1_action = self.disable - if self.local_plugin.has_settings(): - to_draw_button4 = True - else: - button1_label = "Enable" - button1_action = self.enable - button2_label = "Uninstall" - button2_action = self.uninstall - has_update = self.plugin.has_update() - if has_update: - button3_label = "Update" - button3_action = self.update + try: + latest_compatible_version = self.latest_compatible_version + except NoCompatibleVersion: + return False else: - button1_label = "Install" - button1_action = self.install - - if to_draw_button1: - ba.buttonwidget(parent=self._root_widget, - position=(width * 0.1, pos), - size=button_size, - on_activate_call=button1_action, - color=b1_color, - textcolor=b_text_color, - button_type='square', - text_scale=1, - label=button1_label) - - if self.plugin.is_installed: - ba.buttonwidget(parent=self._root_widget, - position=(width * 0.4, pos), - size=button_size, - on_activate_call=button2_action, - color=b2_color, - textcolor=b_text_color, - button_type='square', - text_scale=1, - label=button2_label) - - if has_update: - # button3 = - ba.buttonwidget(parent=self._root_widget, - position=(width * 0.7, pos), - size=button_size, - on_activate_call=button3_action, - color=b3_color, - textcolor=b_text_color, - autoselect=True, - button_type='square', - text_scale=1, - label=button3_label) - ba.containerwidget(edit=self._root_widget, - on_cancel_call=self._ok) - - open_pos_x = (300 if _uiscale is ba.UIScale.SMALL else - 360 if _uiscale is ba.UIScale.MEDIUM else 350) - open_pos_y = (100 if _uiscale is ba.UIScale.SMALL else - 110 if _uiscale is ba.UIScale.MEDIUM else 120) - open_button = ba.buttonwidget(parent=self._root_widget, - autoselect=True, - position=(open_pos_x-7.5, open_pos_y-15), - size=(55, 55), - button_type="square", - label="", - on_activate_call=lambda: ba.open_url(self.plugin.view_url)) - ba.imagewidget(parent=self._root_widget, - position=(open_pos_x, open_pos_y), - size=(40, 40), - color=(0.8, 0.95, 1), - texture=ba.gettexture("file"), - draw_controller=open_button) - ba.textwidget(parent=self._root_widget, - position=(open_pos_x, open_pos_y-6), - text="Source", - size=(10, 10), - scale=0.5) - - if to_draw_button4: - settings_pos_x = (0 if _uiscale is ba.UIScale.SMALL else - 60 if _uiscale is ba.UIScale.MEDIUM else 60) - settings_pos_y = (100 if _uiscale is ba.UIScale.SMALL else - 110 if _uiscale is ba.UIScale.MEDIUM else 120) - settings_button = ba.buttonwidget(parent=self._root_widget, - autoselect=True, - position=(settings_pos_x, settings_pos_y), - size=(40, 40), - button_type="square", - label="", - on_activate_call=self.settings) - ba.imagewidget(parent=self._root_widget, - position=(settings_pos_x, settings_pos_y), - size=(40, 40), - color=(0.8, 0.95, 1), - texture=ba.gettexture("settingsIcon"), - draw_controller=settings_button) - - # ba.containerwidget(edit=self._root_widget, selected_child=button3) - # ba.containerwidget(edit=self._root_widget, start_button=button3) - - def _ok(self) -> None: - play_sound() - ba.containerwidget(edit=self._root_widget, transition='out_scale') - - def button(fn): - async def asyncio_handler(fn, self, *args, **kwargs): - await fn(self, *args, **kwargs) - await self.button_callback() - - def wrapper(self, *args, **kwargs): - self._ok() - loop = asyncio.get_event_loop() - if asyncio.iscoroutinefunction(fn): - loop.create_task(asyncio_handler(fn, self, *args, **kwargs)) - else: - fn(self, *args, **kwargs) - loop.create_task(self.button_callback()) - - return wrapper - - def settings(self): - self.local_plugin.launch_settings() - - @button - def disable(self) -> None: - self.local_plugin.disable() - - @button - async def enable(self) -> None: - await self.local_plugin.enable() - - @button - async def install(self): - await self.plugin.latest_compatible_version.install() - - @button - async def uninstall(self): - await self.plugin.uninstall() + return self.get_local().version != latest_compatible_version.number - @button async def update(self): - await self.plugin.update() + if await self.latest_compatible_version.install(suppress_screenmessage=True): + bui.screenmessage(f"{self.name} updated to {self.latest_compatible_version.number}", + color=(0, 1, 0)) + bui.getsound('shieldUp').play() + else: + bui.screenmessage(f"{self.name} failed MD5 checksum while updating to " + f"{self.latest_compatible_version.number}", + color=(1, 0, 0)) + bui.getsound('error').play() class PluginManager: def __init__(self): self.request_headers = HEADERS self._index = _CACHE.get("index", {}) + self._changelog = _CACHE.get("changelog", {}) self.categories = {} self.module_path = sys.modules[__name__].__file__ + self._index_setup_in_progress = False + self._changelog_setup_in_progress = False async def get_index(self): if not self._index: @@ -828,27 +871,91 @@ async def get_index(self): headers=self.request_headers, ) response = await async_send_network_request(request) - self._index = json.loads(response.read()) - self.set_index_global_cache(self._index) + index = json.loads(response.read()) + self.set_index_global_cache(index) + self._index = index return self._index async def setup_index(self): + while self._index_setup_in_progress: + # Avoid making multiple network calls to the same resource in parallel. + # Rather wait for the previous network call to complete. + await asyncio.sleep(0.1) + self._index_setup_in_progress = not bool(self._index) index = await self.get_index() await self.setup_plugin_categories(index) + self._index_setup_in_progress = False + + async def get_changelog(self) -> list[str, bool]: + requested = False + if not self._changelog: + request = urllib.request.Request(CHANGELOG_META.format( + repository_url=REPOSITORY_URL, + content_type="raw", + tag=CURRENT_TAG + ), + headers=self.request_headers) + response = await async_send_network_request(request) + self._changelog = response.read().decode() + requested = True + return [self._changelog, requested] + + async def setup_changelog(self, version=None) -> None: + if version is None: + version = PLUGIN_MANAGER_VERSION + while self._changelog_setup_in_progress: + # Avoid making multiple network calls to the same resource in parallel. + # Rather wait for the previous network call to complete. + await asyncio.sleep(0.1) + self._changelog_setup_in_progress = not bool(self._changelog) + try: + full_changelog = await self.get_changelog() + # check if the changelog was requested + if full_changelog[1]: + pattern = rf"### {version} \(\d\d-\d\d-\d{{4}}\)\n(.*?)(?=### \d+\.\d+\.\d+|\Z)" + if (len(full_changelog[0].split(version)) > 1): + released_on = full_changelog[0].split(version)[1].split('\n')[0] + matches = re.findall(pattern, full_changelog[0], re.DOTALL) + else: + matches = None + + if matches: + changelog = { + 'released_on': released_on, + 'info': matches[0].strip() + } + else: + changelog = {'released_on': ' (Not Provided)', + 'info': f"Changelog entry for version {version} not found."} + else: + changelog = full_changelog[0] + except urllib.error.URLError: + changelog = {'released_on': ' (Not Provided)', + 'info': 'Could not get ChangeLog due to Internet Issues.'} + self.set_changelog_global_cache(changelog) + self._changelog_setup_in_progress = False async def setup_plugin_categories(self, plugin_index): # A hack to have the "All" category show at the top. self.categories["All"] = None requests = [] - for plugin_category_url in plugin_index["categories"]: - category = Category(plugin_category_url) + for meta_url in plugin_index["categories"]: + category = Category(meta_url) request = category.fetch_metadata() requests.append(request) - for repository in ba.app.config["Community Plugin Manager"]["Custom Sources"]: - plugin_category_url = partial_format(plugin_index["external_source_url"], - repository=repository) - category = Category(plugin_category_url, is_3rd_party=True) + for source in babase.app.config["Community Plugin Manager"]["Custom Sources"]: + source_splits = source.split("@", maxsplit=1) + if len(source_splits) == 1: + # Fallack to `main` if `@branchname` isn't specified in an external source URI. + source_repo, source_tag = source_splits[0], "main" + else: + source_repo, source_tag = source_splits + meta_url = partial_format( + plugin_index["external_source_url"], + repository=source_repo, + ) + category = Category(meta_url, tag=source_tag) request = category.fetch_metadata() requests.append(request) categories = await asyncio.gather(*requests) @@ -861,9 +968,11 @@ async def setup_plugin_categories(self, plugin_index): def cleanup(self): for category in self.categories.values(): - category.cleanup() + if category is not None: + category.cleanup() self.categories.clear() self._index.clear() + self._changelog = None self.unset_index_global_cache() async def refresh(self): @@ -873,16 +982,20 @@ async def refresh(self): def set_index_global_cache(self, index): _CACHE["index"] = index + def set_changelog_global_cache(self, changelog): + _CACHE["changelog"] = changelog + def unset_index_global_cache(self): try: del _CACHE["index"] + del _CACHE["changelog"] except KeyError: pass async def get_update_details(self): index = await self.get_index() for version, info in index["versions"].items(): - if info["api_version"] != ba.app.api_version: + if info["api_version"] != _app_api_version: # No point checking a version of the API game doesn't support. continue if version == PLUGIN_MANAGER_VERSION: @@ -910,7 +1023,7 @@ async def update(self, to_version=None, commit_sha=None): response = await async_send_network_request(download_url) content = response.read() if hashlib.md5(content).hexdigest() != to_version_info["md5sum"]: - raise TypeError("md5sum check failed") + raise MD5CheckSumFailed("MD5 checksum failed during plugin manager update.") with open(self.module_path, "wb") as fout: fout.write(content) return to_version_info @@ -919,196 +1032,815 @@ async def soft_refresh(self): pass -class PluginSourcesWindow(popup.PopupWindow): +class ChangelogWindow(popup.PopupWindow): def __init__(self, origin_widget): - play_sound() - self.scale_origin = origin_widget.get_screen_space_center() - - b_textcolor = (0.75, 0.7, 0.8) - # s = 1.1 if _uiscale is ba.UIScale.SMALL else 1.27 if ba.UIScale.MEDIUM else 1.57 - # text_scale = 0.7 * s + bui.getsound('swish').play() + s = 1.65 if _uiscale() is babase.UIScale.SMALL else 1.39 if _uiscale() is babase.UIScale.MEDIUM else 1.67 + width = 400 * s + height = width * 0.5 + color = (1, 1, 1) + text_scale = 0.7 * s self._transition_out = 'out_scale' transition = 'in_scale' - self._root_widget = ba.containerwidget(size=(400, 340), - # parent=_ba.get_special_widget( - # 'overlay_stack'), - on_outside_click_call=self._ok, - transition=transition, - scale=(2.1 if _uiscale is ba.UIScale.SMALL else 1.5 - if _uiscale is ba.UIScale.MEDIUM else 1.0), - scale_origin_stack_offset=self.scale_origin, - on_cancel_call=self._ok) - - ba.textwidget( - parent=self._root_widget, - position=(155, 300), - size=(100, 25), - text="Custom Plugin Sources", - color=ba.app.ui.title_color, - scale=0.8, - h_align="center", - v_align="center", - maxwidth=270, + + self._root_widget = bui.containerwidget( + size=(width, height), + on_outside_click_call=self._back, + transition=transition, + scale=(1.5 if _uiscale() is babase.UIScale.SMALL else 1.5 if _uiscale() + is babase.UIScale.MEDIUM else 1.0), + scale_origin_stack_offset=self.scale_origin ) - scroll_size_x = (290 if _uiscale is ba.UIScale.SMALL else - 300 if _uiscale is ba.UIScale.MEDIUM else 290) - scroll_size_y = (170 if _uiscale is ba.UIScale.SMALL else - 185 if _uiscale is ba.UIScale.MEDIUM else 180) - scroll_pos_x = (55 if _uiscale is ba.UIScale.SMALL else - 40 if _uiscale is ba.UIScale.MEDIUM else 60) - scroll_pos_y = 105 + _add_popup(self) - self._scrollwidget = ba.scrollwidget(parent=self._root_widget, - size=(scroll_size_x, scroll_size_y), - position=(scroll_pos_x, scroll_pos_y)) - self._columnwidget = ba.columnwidget(parent=self._scrollwidget, - border=1, - margin=0) + bui.textwidget( + parent=self._root_widget, + position=(width * 0.49, height * 0.87), + size=(0, 0), + h_align='center', + v_align='center', + text='ChangeLog', + scale=text_scale * 1.25, + color=bui.app.ui_v1.title_color, + maxwidth=width * 0.9 + ) - delete_source_button_position_pos_x = 360 - delete_source_button_position_pos_y = 110 - delete_source_button = ba.buttonwidget(parent=self._root_widget, - position=(delete_source_button_position_pos_x, - delete_source_button_position_pos_y), - size=(25, 25), - on_activate_call=self.delete_selected_source, - label="", - # texture=ba.gettexture("crossOut"), - button_type="square", - color=(0.6, 0, 0), - textcolor=b_textcolor, - # autoselect=True, - text_scale=1) - - ba.imagewidget(parent=self._root_widget, - position=(delete_source_button_position_pos_x + 2, - delete_source_button_position_pos_y), - size=(25, 25), - color=(5, 2, 2), - texture=ba.gettexture("crossOut"), - draw_controller=delete_source_button) - ba.textwidget( + back_button = bui.buttonwidget( parent=self._root_widget, - position=(48, 74), - size=(50, 22), - text=("Warning: 3rd party plugin sources are not moderated\n" - " by the community and may be dangerous!"), - color=(1, 0.23, 0.23), - scale=0.5, - h_align="left", - v_align="center", - maxwidth=400, + position=(width * 0.1, height * 0.8), + size=(60, 60), + scale=0.8, + label=babase.charstr(babase.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self._back ) - self._add_source_widget = ba.textwidget(parent=self._root_widget, - text="rikkolovescats/sahilp-plugins", - size=(335, 50), - position=(21, 22), - h_align='left', - v_align='center', - editable=True, - scale=0.75, - maxwidth=215, - # autoselect=True, - description="Add Source") - - loop = asyncio.get_event_loop() - - ba.buttonwidget(parent=self._root_widget, - position=(330, 28), - size=(37, 37), - on_activate_call=lambda: loop.create_task(self.add_source()), - label="", - texture=ba.gettexture("startButton"), - # texture=ba.gettexture("chestOpenIcon"), - button_type="square", - color=(0, 0.9, 0), - textcolor=b_textcolor, - # autoselect=True, - text_scale=1) + bui.containerwidget(edit=self._root_widget, cancel_button=back_button) - self.draw_sources() + try: + released_on = _CACHE['changelog']['released_on'] + logs = _CACHE['changelog']['info'].split('\n') + h_align = 'left' + extra = 0.1 + except KeyError: + released_on = '' + logs = ["Could not load ChangeLog"] + h_align = 'center' + extra = 1 - def draw_sources(self): - for plugin in self._columnwidget.get_children(): - plugin.delete() + bui.textwidget( + parent=self._root_widget, + position=(width * 0.49, height * 0.72), + size=(0, 0), + h_align='center', + v_align='center', + text=PLUGIN_MANAGER_VERSION + released_on, + scale=text_scale * 0.9, + color=color, + maxwidth=width * 0.9 + ) - color = (1, 1, 1) - for custom_source in ba.app.config["Community Plugin Manager"]["Custom Sources"]: - ba.textwidget(parent=self._columnwidget, - # size=(410, 30), - selectable=True, - # always_highlight=True, - color=color, - text=custom_source, - # click_activate=True, - on_select_call=lambda: self.select_source(custom_source), - h_align='left', - v_align='center', - scale=0.75, - maxwidth=260) + bui.buttonwidget( + parent=self._root_widget, + position=(width * 0.7, height * 0.72 - 20), + size=(140, 60), + scale=0.8, + label='Full ChangeLog', + button_type='square', + on_activate_call=lambda: bui.open_url(REPOSITORY_URL + '/blob/main/CHANGELOG.md') + ) + + loop_height = height * 0.62 + for log in logs: + bui.textwidget( + parent=self._root_widget, + position=(width * 0.5 * extra, loop_height), + size=(0, 0), + h_align=h_align, + v_align='top', + text=log, + scale=text_scale, + color=color, + maxwidth=width * 0.9 + ) + loop_height -= 35 + + def _back(self) -> None: + bui.getsound('swish').play() + _remove_popup(self) + bui.containerwidget(edit=self._root_widget, transition='out_scale') + + +class AuthorsWindow(popup.PopupWindow): + def __init__(self, authors_info, origin_widget): + self.authors_info = authors_info + self.scale_origin = origin_widget.get_screen_space_center() + bui.getsound('swish').play() + s = 1.25 if _uiscale() is babase.UIScale.SMALL else 1.39 if _uiscale() is babase.UIScale.MEDIUM else 1.67 + width = 400 * s + height = width * 0.8 + color = (1, 1, 1) + text_scale = 0.7 * s + self._transition_out = 'out_scale' + transition = 'in_scale' + + self._root_widget = bui.containerwidget( + size=(width, height), + on_outside_click_call=self._back, + transition=transition, + scale=(1.5 if _uiscale() is babase.UIScale.SMALL else 1.5 + if _uiscale() is babase.UIScale.MEDIUM else 1.0), + scale_origin_stack_offset=self.scale_origin + ) + + _add_popup(self) + + pos = height * 0.9 + bui.textwidget( + parent=self._root_widget, + position=(width * 0.49, pos), + size=(0, 0), + h_align='center', + v_align='center', + text='Authors', + scale=text_scale * 1.25, + color=color, + maxwidth=width * 0.9 + ) + + back_button = bui.buttonwidget( + parent=self._root_widget, + position=(width * 0.1, height * 0.87), + size=(60, 60), + scale=0.8, + label=babase.charstr(babase.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self._back + ) + + bui.containerwidget(edit=self._root_widget, cancel_button=back_button) + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + size=(width * 0.8, height * 0.75), + position=(width * 0.1, height * 0.1) + ) + self._columnwidget = bui.columnwidget( + parent=self._scrollwidget, + border=1, + left_border=-15, + margin=0 + ) + + for author in self.authors_info: + for key, value in author.items(): + text = f"{key.title()}: {value if value != '' else 'Not Provided'}" + if key == 'name': + text = value + bui.textwidget( + parent=self._columnwidget, + size=(width * 0.8, 35 if key == 'name' else 30), + color=color if key == 'name' else (0.75, 0.7, 0.8), + scale=( + (1.1 if key == 'name' else 0.9) if _uiscale() is babase.UIScale.SMALL else + (1.2 if key == 'name' else 1.0) + ), + text=text, + h_align='center', + v_align='center', + maxwidth=420 + ) + bui.textwidget( + parent=self._columnwidget, + size=(width * 0.8, 30), + always_highlight=True, + h_align='center', + v_align='center' + ) + + def _back(self) -> None: + bui.getsound('swish').play() + _remove_popup(self) + bui.containerwidget(edit=self._root_widget, transition='out_scale') + + +class PluginWindow(popup.PopupWindow): + def __init__( + self, + plugin: Plugin, + origin_widget, + plugins_list, + transition='in_scale', + button_callback=lambda: None, + ): + self.plugin: Plugin = plugin + self.transition = transition + self.plugins_list = plugins_list + self.button_callback = button_callback + self.scale_origin = origin_widget.get_screen_space_center() + + loop.create_task(self.draw_ui()) + + def get_description(self, minimum_character_offset=40): + """ + Splits the long plugin description into multiple lines. + """ + string = self.plugin.info["description"] + string_length = len(string) + + partitioned_string = "" + partitioned_string_length = len(partitioned_string) + + while partitioned_string_length != string_length: + next_empty_space = string[partitioned_string_length + + minimum_character_offset:].find(" ") + next_word_end_position = partitioned_string_length + \ + minimum_character_offset + max(0, next_empty_space) + partitioned_string += string[partitioned_string_length:next_word_end_position] + if next_empty_space != -1: + # Insert a line break here, there's still more partitioning to do. + partitioned_string += "\n" + partitioned_string_length = len(partitioned_string) + + return partitioned_string + + async def draw_ui(self): + bui.getsound('swish').play() + b_text_color = (0.75, 0.7, 0.8) + s = 1.25 if _uiscale() is babase.UIScale.SMALL else 1.39 if babase.UIScale.MEDIUM else 1.67 + width = 450 * s + height = 120 + 100 * s + color = (1, 1, 1) + text_scale = 0.7 * s + + self._root_widget = bui.containerwidget( + size=(width, height), + on_outside_click_call=self._cancel, + transition=self.transition, + scale=(2.1 if _uiscale() is babase.UIScale.SMALL else 1.5 + if _uiscale() is babase.UIScale.MEDIUM else 1.0), + scale_origin_stack_offset=self.scale_origin + ) + + _add_popup(self) + + i = self.plugins_list.index(self.plugin) + self.p_n_plugins = [ + self.plugins_list[i-1] if (i-1 > -1) else None, + self.plugins_list[i+1] if (i+1 < len(self.plugins_list)) else None + ] + + if self.p_n_plugins is not None: + if self.p_n_plugins[0] is not None: + previous_plugin_button = bui.buttonwidget( + parent=self._root_widget, + position=(-12.5*s + (4 if _uiscale() is babase.UIScale.SMALL else -5), + height/2 - 20*s), + label='<', + size=(25, 40), + color=(1, 0.5, 0.5), + scale=s, + on_activate_call=self.show_previous_plugin + ) + + if self.p_n_plugins[1] is not None: + next_plugin_button = bui.buttonwidget( + parent=self._root_widget, + position=(width - 12.5*s - (8 if _uiscale() + is babase.UIScale.SMALL else 0), height/2 - 20*s), + label='>', + size=(25, 40), + color=(1, 0.5, 0.5), + scale=s, + on_activate_call=self.show_next_plugin + ) + + pos = height * 0.8 + plug_name = self.plugin.name.replace('_', ' ').title() + plugin_title = f"{plug_name} (v{self.plugin.latest_compatible_version.number})" + bui.textwidget( + parent=self._root_widget, + position=(width * 0.49, pos), + size=(0, 0), + h_align='center', + v_align='center', + text=plugin_title, + scale=text_scale * 1.25, + color=color, + maxwidth=width * 0.9 + ) + pos -= 25 + # Author + text = 'by ' + ', '.join([author["name"] for author in self.plugin.info["authors"]]) + author_text_control_btn = bui.buttonwidget( + parent=self._root_widget, + position=(width * 0.49 - (len(text)*14/2), pos - 10), + size=(len(text)*14, 20), + label='', + texture=bui.gettexture("empty"), + on_activate_call=lambda: AuthorsWindow(self.plugin.info["authors"], self._root_widget) + ) + bui.textwidget( + parent=self._root_widget, + position=(width * 0.49 - (len(text)*14/2), pos - 10), + size=(len(text)*14, 20), + h_align='center', + v_align='center', + text=text, + scale=text_scale * 0.8, + color=(0.75, 0.7, 0.8), + maxwidth=width * 0.9, + draw_controller=author_text_control_btn, + ) + pos -= 60 + # Info + bui.textwidget( + parent=self._root_widget, + position=(width * 0.49, pos), + size=(0, 0), + h_align='center', + v_align='center', + text=self.get_description(), + scale=text_scale * 0.6, + color=color, + maxwidth=width * 0.95 + ) + b1_color = None + b2_color = (0.8, 0.15, 0.35) + b3_color = (0.2, 0.8, 0.3) + pos = height * 0.1 + button_size = (80 * s, 40 * s) + + to_draw_button1 = True + to_draw_button4 = False + if self.plugin.is_installed: + self.local_plugin = self.plugin.get_local() + if not await self.local_plugin.has_plugins(): + to_draw_button1 = False + else: + if await self.local_plugin.is_enabled(): + button1_label = "Disable" + b1_color = (0.6, 0.53, 0.63) + button1_action = self.disable + if self.local_plugin.has_settings(): + to_draw_button4 = True + else: + button1_label = "Enable" + button1_action = self.enable + button2_label = "Uninstall" + button2_action = self.uninstall + has_update = self.plugin.has_update() + if has_update: + button3_label = "Update" + button3_action = self.update + else: + button1_label = "Install" + button1_action = self.install + + if to_draw_button1: + selected_btn = bui.buttonwidget( + parent=self._root_widget, + position=( + width * (0.1 if self.plugin.is_installed and has_update else + 0.25 if self.plugin.is_installed else 0.4), pos + ), + size=button_size, + on_activate_call=button1_action, + color=b1_color, + textcolor=b_text_color, + button_type='square', + text_scale=1, + label=button1_label + ) + + if self.plugin.is_installed: + selected_btn = bui.buttonwidget( + parent=self._root_widget, + position=( + width * (0.4 if has_update or not to_draw_button1 else 0.55), pos), + size=button_size, + on_activate_call=button2_action, + color=b2_color, + textcolor=b_text_color, + button_type='square', + text_scale=1, + label=button2_label + ) + + if has_update: + selected_btn = bui.buttonwidget( + parent=self._root_widget, + position=(width * 0.7, pos), + size=button_size, + on_activate_call=button3_action, + color=b3_color, + textcolor=b_text_color, + autoselect=True, + button_type='square', + text_scale=1, + label=button3_label + ) + + bui.containerwidget( + edit=self._root_widget, + on_cancel_call=self._cancel, + selected_child=selected_btn + ) + + open_pos_x = (390 if _uiscale() is babase.UIScale.SMALL else + 450 if _uiscale() is babase.UIScale.MEDIUM else 440) + open_pos_y = (100 if _uiscale() is babase.UIScale.SMALL else + 110 if _uiscale() is babase.UIScale.MEDIUM else 120) + open_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(open_pos_x, open_pos_y), + size=(40, 40), + button_type="square", + label="", + color=(0.6, 0.53, 0.63), + on_activate_call=lambda: bui.open_url(self.plugin.view_url) + ) + bui.imagewidget( + parent=self._root_widget, + position=(open_pos_x, open_pos_y), + size=(40, 40), + color=(0.8, 0.95, 1), + texture=bui.gettexture("file"), + draw_controller=open_button + ) + bui.textwidget( + parent=self._root_widget, + position=(open_pos_x-3, open_pos_y+12), + text="Source", + size=(10, 10), + draw_controller=open_button, + color=(1, 1, 1, 1), + rotate=25, + scale=0.45 + ) + + # Below snippet handles the tutorial button in the plugin window + tutorial_url = self.plugin.info["external_url"] + if tutorial_url: + def tutorial_confirm_window(): + text = "This will take you to \n\""+self.plugin.info["external_url"] + "\"" + tutorial_confirm_window = confirm.ConfirmWindow( + text=text, + action=lambda: bui.open_url(self.plugin.info["external_url"]), + ) + open_pos_x = (440 if _uiscale() is babase.UIScale.SMALL else + 500 if _uiscale() is babase.UIScale.MEDIUM else 490) + open_pos_y = (100 if _uiscale() is babase.UIScale.SMALL else + 110 if _uiscale() is babase.UIScale.MEDIUM else 120) + open_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(open_pos_x, open_pos_y), + size=(40, 40), + button_type="square", + label="", + color=(0.6, 0.53, 0.63), + on_activate_call=tutorial_confirm_window + ) + + bui.imagewidget( + parent=self._root_widget, + position=(open_pos_x, open_pos_y), + size=(40, 40), + color=(0.8, 0.95, 1), + texture=bui.gettexture("frameInset"), + draw_controller=open_button + ) + bui.textwidget( + parent=self._root_widget, + position=(open_pos_x - 3, open_pos_y + 12), + text="Tutorial", + size=(10, 10), + draw_controller=open_button, + color=(1, 1, 1, 1), + rotate=25, + scale=0.45 + ) + + if to_draw_button4: + settings_pos_x = (60 if _uiscale() is babase.UIScale.SMALL else + 60 if _uiscale() is babase.UIScale.MEDIUM else 60) + settings_pos_y = (100 if _uiscale() is babase.UIScale.SMALL else + 110 if _uiscale() is babase.UIScale.MEDIUM else 120) + settings_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(settings_pos_x, settings_pos_y), + size=(40, 40), + button_type="square", + label="", + color=(0, 0.75, 0.75) + ) + bui.buttonwidget( + edit=settings_button, + on_activate_call=babase.Call(self.settings, settings_button) + ) + bui.imagewidget( + parent=self._root_widget, + position=(settings_pos_x, settings_pos_y), + size=(40, 40), + color=(0.8, 0.95, 1), + texture=bui.gettexture("settingsIcon"), + draw_controller=settings_button + ) + + def _ok(self) -> None: + _remove_popup(self) + bui.containerwidget(edit=self._root_widget, transition='out_scale') + + def _cancel(self) -> None: + bui.getsound('swish').play() + _remove_popup(self) + bui.containerwidget(edit=self._root_widget, transition='out_scale') + + def button(fn): + async def asyncio_handler(fn, self, *args, **kwargs): + await fn(self, *args, **kwargs) + await self.button_callback() + + def wrapper(self, *args, **kwargs): + self._ok() + + if asyncio.iscoroutinefunction(fn): + loop.create_task(asyncio_handler(fn, self, *args, **kwargs)) + else: + fn(self, *args, **kwargs) + loop.create_task(self.button_callback()) + + return wrapper + + def settings(self, source_widget): + self.local_plugin.launch_settings(source_widget) + + def show_previous_plugin(self): + bui.containerwidget(edit=self._root_widget, transition='out_right') + _remove_popup(self) + PluginWindow( + self.p_n_plugins[0], + self._root_widget, + transition='in_left', + plugins_list=self.plugins_list, + button_callback=lambda: None + ) + + def show_next_plugin(self): + bui.containerwidget(edit=self._root_widget, transition='out_left') + _remove_popup(self) + PluginWindow( + self.p_n_plugins[1], + self._root_widget, + transition='in_right', + plugins_list=self.plugins_list, + button_callback=lambda: None + ) + + @button + def disable(self) -> None: + self.local_plugin.disable() + + @button + async def enable(self) -> None: + await self.local_plugin.enable() + bui.getsound('gunCocking').play() + + @button + async def install(self): + await self.plugin.latest_compatible_version.install() + bui.getsound('cashRegister2').play() + + @button + async def uninstall(self): + await self.plugin.uninstall() + bui.getsound('shieldDown').play() + + @button + async def update(self): + await self.plugin.update() + bui.getsound('shieldUp').play() + + +class PluginCustomSourcesWindow(popup.PopupWindow): + def __init__(self, origin_widget): + self.selected_source = None + + self.scale_origin = origin_widget.get_screen_space_center() + + b_textcolor = (0.75, 0.7, 0.8) + self._transition_out = 'out_scale' + transition = 'in_scale' + self._root_widget = bui.containerwidget( + size=(400, 340), + on_outside_click_call=self._ok, + transition=transition, + scale=(2.1 if _uiscale() is babase.UIScale.SMALL else 1.5 + if _uiscale() is babase.UIScale.MEDIUM else 1.0), + scale_origin_stack_offset=self.scale_origin, + on_cancel_call=self._ok + ) + + _add_popup(self) + + bui.textwidget( + parent=self._root_widget, + position=(155, 300), + size=(100, 25), + text="Custom Plugin Sources", + color=bui.app.ui_v1.title_color, + scale=0.8, + h_align="center", + v_align="center", + maxwidth=270, + ) + + scroll_size_x = (290 if _uiscale() is babase.UIScale.SMALL else + 300 if _uiscale() is babase.UIScale.MEDIUM else 290) + scroll_size_y = (170 if _uiscale() is babase.UIScale.SMALL else + 185 if _uiscale() is babase.UIScale.MEDIUM else 180) + scroll_pos_x = (55 if _uiscale() is babase.UIScale.SMALL else + 40 if _uiscale() is babase.UIScale.MEDIUM else 60) + scroll_pos_y = 105 + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + size=(scroll_size_x, scroll_size_y), + position=(scroll_pos_x, scroll_pos_y) + ) + self._columnwidget = bui.columnwidget( + parent=self._scrollwidget, + border=1, margin=0 + ) + + delete_source_button_position_pos_x = 360 + delete_source_button_position_pos_y = 110 + delete_source_button = bui.buttonwidget( + parent=self._root_widget, + position=( + delete_source_button_position_pos_x, delete_source_button_position_pos_y + ), + size=(25, 25), + label="", + on_activate_call=self.delete_selected_source, + button_type="square", + color=(0.6, 0, 0) + ) + + bui.imagewidget( + parent=self._root_widget, + position=( + delete_source_button_position_pos_x + 2, delete_source_button_position_pos_y + ), + size=(25, 25), + color=(5, 2, 2), + texture=bui.gettexture("crossOut"), + draw_controller=delete_source_button + ) + + warning_pos_x = (43 if _uiscale() is babase.UIScale.SMALL else + 35 if _uiscale() is babase.UIScale.MEDIUM else + 48) + bui.textwidget( + parent=self._root_widget, + position=(warning_pos_x, 74), + size=(50, 22), + text=("Warning: 3rd party plugin sources are not moderated\n" + " by the community and may be dangerous!"), + color=(1, 0.23, 0.23), + scale=0.5, + h_align="left", + v_align="center", + maxwidth=400, + ) + + self._add_source_widget = bui.textwidget( + parent=self._root_widget, + size=(335, 50), + position=(21, 22), + h_align='left', + v_align='center', + editable=True, + scale=0.75, + maxwidth=215, + description="Add Source" + ) + + bui.buttonwidget( + parent=self._root_widget, + position=(330, 28), + size=(37, 37), + on_activate_call=lambda: loop.create_task(self.add_source()), + label="", + texture=bui.gettexture("startButton"), + button_type="square", + color=(0, 0.9, 0), + textcolor=b_textcolor, + text_scale=1 + ) + + self.draw_sources() + + def draw_sources(self): + for plugin in self._columnwidget.get_children(): + plugin.delete() + + color = (1, 1, 1) + for custom_source in babase.app.config["Community Plugin Manager"]["Custom Sources"]: + bui.textwidget( + parent=self._columnwidget, + selectable=True, + color=color, + text=custom_source, + on_select_call=lambda: self.select_source(custom_source), + h_align='left', + v_align='center', + scale=0.75, + maxwidth=260 + ) def select_source(self, source): self.selected_source = source async def add_source(self): - source = ba.textwidget(query=self._add_source_widget) - meta_url = _CACHE["index"]["external_source_url"].format( - repository=source, - content_type="raw", - tag=CURRENT_TAG + source = bui.textwidget(query=self._add_source_widget) + # External source URIs can optionally suffix `@branchname`, for example: + # `bombsquad-community/sample-plugin-source@experimental` + source_splits = source.split("@", maxsplit=1) + if len(source_splits) == 1: + # Fallack to `main` if `@branchname` isn't specified in an external source URI. + source_repo, source_tag = source_splits[0], "main" + else: + source_repo, source_tag = source_splits + meta_url = partial_format( + _CACHE["index"]["external_source_url"], + repository=source_repo, ) - category = Category(meta_url, is_3rd_party=True) - if not await category.is_valid(): - ba.screenmessage("Enter a valid plugin source", color=(1, 0, 0)) + category = Category(meta_url, tag=source_tag) + try: + await category.validate() + except (PluginSourceNetworkError, CategoryMetadataParseError) as e: + bui.screenmessage(str(e), color=(1, 0, 0)) + bui.getsound('error').play() return - if source in ba.app.config["Community Plugin Manager"]["Custom Sources"]: - ba.screenmessage("Plugin source already exists") + if source in babase.app.config["Community Plugin Manager"]["Custom Sources"]: + bui.screenmessage("Plugin source already exists") + bui.getsound('error').play() return - ba.app.config["Community Plugin Manager"]["Custom Sources"].append(source) - ba.app.config.commit() - ba.screenmessage("Plugin source added, refresh plugin list to see changes", - color=(0, 1, 0)) + babase.app.config["Community Plugin Manager"]["Custom Sources"].append(source) + babase.app.config.commit() + bui.screenmessage("Plugin source added; Refresh plugin list to see changes", + color=(0, 1, 0)) + bui.getsound('cashRegister2').play() self.draw_sources() def delete_selected_source(self): - try: - ba.app.config["Community Plugin Manager"]["Custom Sources"].remove(self.selected_source) - ba.app.config.commit() - ba.screenmessage("Plugin source deleted, refresh plugin list to see changes", - color=(0, 1, 0)) - self.draw_sources() - except Exception: - ba.screenmessage("No Plugin Selected to Delete", color=(1, 0, 0)) + if self.selected_source is None: + return + babase.app.config["Community Plugin Manager"]["Custom Sources"].remove(self.selected_source) + babase.app.config.commit() + bui.screenmessage("Plugin source deleted; Refresh plugin list to see changes", + color=(0.9, 1, 0)) + bui.getsound('shieldDown').play() + self.draw_sources() def _ok(self) -> None: - play_sound() - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.getsound('swish').play() + _remove_popup(self) + bui.containerwidget(edit=self._root_widget, transition='out_scale') class PluginCategoryWindow(popup.PopupMenuWindow): def __init__(self, choices, current_choice, origin_widget, asyncio_callback): - choices = (*choices, "Custom Sources") + choices = (*choices, "Installed", "Custom Sources") self._asyncio_callback = asyncio_callback self.scale_origin = origin_widget.get_screen_space_center() super().__init__( - position=(200, 0), - scale=(2.3 if _uiscale is ba.UIScale.SMALL else - 1.65 if _uiscale is ba.UIScale.MEDIUM else 1.23), + position=self.scale_origin, + scale=(2.3 if _uiscale() is babase.UIScale.SMALL else + 1.65 if _uiscale() is babase.UIScale.MEDIUM else 1.23), choices=choices, current_choice=current_choice, - delegate=self) + delegate=self + ) + self._root_widget = self.root_widget + _add_popup(self) self._update_custom_sources_widget() def _update_custom_sources_widget(self): - ba.textwidget(edit=self._columnwidget.get_children()[-1], - color=(0.5, 0.5, 0.5), - on_activate_call=self.show_sources_window) + bui.textwidget( + edit=self._columnwidget.get_children()[-1], + color=(0.5, 0.5, 0.5), + on_activate_call=self.show_sources_window + ) def popup_menu_selected_choice(self, window, choice): - loop = asyncio.get_event_loop() + loop.create_task(self._asyncio_callback(choice)) def popup_menu_closing(self, window): @@ -1116,108 +1848,163 @@ def popup_menu_closing(self, window): def show_sources_window(self): self._ok() - PluginSourcesWindow(origin_widget=self.root_widget) + PluginCustomSourcesWindow(origin_widget=self.root_widget) def _ok(self) -> None: - play_sound() - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.getsound('swish').play() + _remove_popup(self) + bui.containerwidget(edit=self.root_widget, transition='out_scale') -class PluginManagerWindow(ba.Window): - def __init__(self, transition: str = "in_right", origin_widget: ba.Widget = None): +class PluginManagerWindow(bui.MainWindow): + def __init__( + self, + transition: str = "in_right", + origin_widget: bui.Widget = None + ): self.plugin_manager = PluginManager() self.category_selection_button = None - self.selected_category = None + self.selected_category = 'All' self.plugins_in_current_view = {} + self.selected_alphabet_order = 'a_z' + self.alphabet_order_selection_button = None + global open_popups + open_popups = [] - loop = asyncio.get_event_loop() loop.create_task(self.draw_index()) - self._width = (490 if _uiscale is ba.UIScale.MEDIUM else 570) - self._height = (500 if _uiscale is ba.UIScale.SMALL - else 380 if _uiscale is ba.UIScale.MEDIUM + self._width = (700 if _uiscale() is babase.UIScale.SMALL + else 550 if _uiscale() is babase.UIScale.MEDIUM + else 570) + self._height = (500 if _uiscale() is babase.UIScale.SMALL + else 422 if _uiscale() is babase.UIScale.MEDIUM else 500) - top_extra = 20 if _uiscale is ba.UIScale.SMALL else 0 + top_extra = 20 if _uiscale() is babase.UIScale.SMALL else 0 if origin_widget: self._transition_out = "out_scale" self._scale_origin = origin_widget.get_screen_space_center() transition = "in_scale" - super().__init__(root_widget=ba.containerwidget( - size=(self._width, self._height + top_extra), + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height + top_extra), + toolbar_visibility="menu_minimal", + scale=(1.9 if _uiscale() is babase.UIScale.SMALL + else 1.5 if _uiscale() is babase.UIScale.MEDIUM + else 1.0), + stack_offset=(0, -25) if _uiscale() is babase.UIScale.SMALL else (0, 0) + ), transition=transition, - toolbar_visibility="menu_minimal", - scale_origin_stack_offset=self._scale_origin, - scale=(1.9 if _uiscale is ba.UIScale.SMALL - else 1.5 if _uiscale is ba.UIScale.MEDIUM - else 1.0), - stack_offset=(0, -25) if _uiscale is ba.UIScale.SMALL else (0, 0) - )) - - back_pos_x = 5 + (10 if _uiscale is ba.UIScale.SMALL else - 27 if _uiscale is ba.UIScale.MEDIUM else 68) - back_pos_y = self._height - (115 if _uiscale is ba.UIScale.SMALL else - 65 if _uiscale is ba.UIScale.MEDIUM else 50) - self._back_button = back_button = ba.buttonwidget( + origin_widget=origin_widget, + ) + + back_pos_x = 5 + (37 if _uiscale() is babase.UIScale.SMALL else + 27 if _uiscale() is babase.UIScale.MEDIUM else 68) + back_pos_y = self._height - (95 if _uiscale() is babase.UIScale.SMALL else + 65 if _uiscale() is babase.UIScale.MEDIUM else 50) + + if _uiscale() is bui.UIScale.SMALL: + self._back_button = None + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) + else: + self._back_button = back_button = bui.buttonwidget( parent=self._root_widget, position=(back_pos_x, back_pos_y), size=(60, 60), scale=0.8, - label=ba.charstr(ba.SpecialChar.BACK), - # autoselect=True, + label=babase.charstr(babase.SpecialChar.BACK), button_type='backSmall', - on_activate_call=self._back) + on_activate_call=self.main_window_back + ) - ba.containerwidget(edit=self._root_widget, cancel_button=back_button) + bui.containerwidget(edit=self._root_widget, cancel_button=back_button) - title_pos = self._height - (100 if _uiscale is ba.UIScale.SMALL else - 50 if _uiscale is ba.UIScale.MEDIUM else 50) - ba.textwidget( + title_pos = self._height - (83 if _uiscale() is babase.UIScale.SMALL else + 50 if _uiscale() is babase.UIScale.MEDIUM else 50) + bui.textwidget( parent=self._root_widget, position=(-10, title_pos), size=(self._width, 25), text="Community Plugin Manager", - color=ba.app.ui.title_color, + color=bui.app.ui_v1.title_color, scale=1.05, h_align="center", v_align="center", maxwidth=270, ) - loading_pos_y = self._height - (235 if _uiscale is ba.UIScale.SMALL else - 220 if _uiscale is ba.UIScale.MEDIUM else 250) + loading_pos_y = self._height - (275 if _uiscale() is babase.UIScale.SMALL else + 235 if _uiscale() is babase.UIScale.MEDIUM else 270) - self._plugin_manager_status_text = ba.textwidget( + self._plugin_manager_status_text = bui.textwidget( parent=self._root_widget, position=(-5, loading_pos_y), size=(self._width, 25), - text="Loading...", - color=ba.app.ui.title_color, + text="", + color=bui.app.ui_v1.title_color, scale=0.7, h_align="center", v_align="center", maxwidth=400, ) + self._loading_spinner = bui.spinnerwidget( + parent=self._root_widget, + position=(self._width * 0.5, loading_pos_y), + style='bomb', + size=48, + ) - def _back(self) -> None: - play_sound() - from bastd.ui.settings.allsettings import AllSettingsWindow - ba.containerwidget(edit=self._root_widget, - transition=self._transition_out) - ba.app.ui.set_main_menu_window( - AllSettingsWindow(transition='in_left').get_root_widget()) + @override + def get_main_window_state(self) -> bui.MainWindowState: + # Support recreating our window for back/refresh purposes. + global open_popups + # Close all open popups if ui changes. + # check pr #390 for more info. + for popup in open_popups: + try: + bui.containerwidget(edit=popup._root_widget, transition='out_scale') + except: + pass + cls = type(self) + return bui.BasicMainWindowState( + create_call=lambda transition, origin_widget: cls( + transition=transition, origin_widget=origin_widget + ) + ) + + def spin(self, show=False): + w = self._loading_spinner + p = self._root_widget + bui.spinnerwidget(w, visible=show) if w.exists( + ) and p.exists() and not p.transitioning_out else None @contextlib.contextmanager def exception_handler(self): try: yield except urllib.error.URLError: - ba.textwidget(edit=self._plugin_manager_status_text, - text="Make sure you are connected\n to the Internet and try again.") + self.spin() + try: + bui.textwidget( + edit=self._plugin_manager_status_text, + text="Make sure you are connected\n to the Internet and try again." + ) + except: + pass + self.plugin_manager._index_setup_in_progress = False except RuntimeError: - # User probably went back before a ba.Window could finish loading. + # User probably went back before a bui.Window could finish loading. pass + except Exception as e: + self.spin() + try: + bui.textwidget(edit=self._plugin_manager_status_text, text=str(e)) + except: + pass + raise async def draw_index(self): self.draw_search_bar() @@ -1226,218 +2013,299 @@ async def draw_index(self): self.draw_refresh_icon() self.draw_settings_icon() with self.exception_handler(): + await self.plugin_manager.setup_changelog() await self.plugin_manager.setup_index() - ba.textwidget(edit=self._plugin_manager_status_text, - text="") + self.spin() + try: + bui.textwidget(edit=self._plugin_manager_status_text, text="") + except: + pass await self.select_category("All") def draw_plugins_scroll_bar(self): - scroll_size_x = (400 if _uiscale is ba.UIScale.SMALL else - 380 if _uiscale is ba.UIScale.MEDIUM else 420) - scroll_size_y = (225 if _uiscale is ba.UIScale.SMALL else - 235 if _uiscale is ba.UIScale.MEDIUM else 335) - scroll_pos_x = (70 if _uiscale is ba.UIScale.SMALL else - 40 if _uiscale is ba.UIScale.MEDIUM else 70) - scroll_pos_y = (125 if _uiscale is ba.UIScale.SMALL else - 30 if _uiscale is ba.UIScale.MEDIUM else 40) - self._scrollwidget = ba.scrollwidget(parent=self._root_widget, - size=(scroll_size_x, scroll_size_y), - position=(scroll_pos_x, scroll_pos_y)) - self._columnwidget = ba.columnwidget(parent=self._scrollwidget, - border=2, - margin=0) + scroll_size_x = (515 if _uiscale() is babase.UIScale.SMALL else + 430 if _uiscale() is babase.UIScale.MEDIUM else 420) + scroll_size_y = (245 if _uiscale() is babase.UIScale.SMALL else + 265 if _uiscale() is babase.UIScale.MEDIUM else 335) + scroll_pos_x = (70 if _uiscale() is babase.UIScale.SMALL else + 50 if _uiscale() is babase.UIScale.MEDIUM else 70) + scroll_pos_y = (100 if _uiscale() is babase.UIScale.SMALL else + 35 if _uiscale() is babase.UIScale.MEDIUM else 40) + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + size=(scroll_size_x, scroll_size_y), + position=(scroll_pos_x, scroll_pos_y) + ) + self._columnwidget = bui.columnwidget( + parent=self._scrollwidget, + border=2, + margin=0 + ) def draw_category_selection_button(self, post_label): - category_pos_x = (330 if _uiscale is ba.UIScale.SMALL else - 285 if _uiscale is ba.UIScale.MEDIUM else 350) - category_pos_y = self._height - (145 if _uiscale is ba.UIScale.SMALL else - 110 if _uiscale is ba.UIScale.MEDIUM else 110) + category_pos_x = (440 if _uiscale() is babase.UIScale.SMALL else + 340 if _uiscale() is babase.UIScale.MEDIUM else 370) + category_pos_y = self._height - (141 if _uiscale() is babase.UIScale.SMALL else + 110 if _uiscale() is babase.UIScale.MEDIUM else 110) b_size = (140, 30) - b_textcolor = (0.75, 0.7, 0.8) - b_color = (0.6, 0.53, 0.63) + b_textcolor = (0.8, 0.8, 0.85) + + if self.alphabet_order_selection_button is None: + self.alphabet_order_selection_button = bui.buttonwidget( + parent=self._root_widget, + size=(40, 30), + position=(category_pos_x - 47, category_pos_y), + label='Z - A' if self.selected_alphabet_order == 'z_a' else 'A - Z', + on_activate_call=lambda: loop.create_task(self._on_order_button_press()), + button_type="square", + textcolor=b_textcolor, + text_scale=0.6 + ) + else: + b = self.alphabet_order_selection_button + bui.buttonwidget( + edit=b, + label=('Z - A' if self.selected_alphabet_order == 'z_a' else 'A - Z') + ) if b.exists() else None label = f"Category: {post_label}" if self.category_selection_button is None: - self.category_selection_button = ba.buttonwidget(parent=self._root_widget, - position=(category_pos_x, - category_pos_y), - size=b_size, - on_activate_call=( - self.show_categories_window), - label=label, - button_type="square", - color=b_color, - textcolor=b_textcolor, - # autoselect=True, - text_scale=0.6) + self.category_selection_button = b = bui.buttonwidget( + parent=self._root_widget, + position=(category_pos_x, category_pos_y), + size=b_size, + label=label, + button_type="square", + textcolor=b_textcolor, + text_scale=0.6 + ) + bui.buttonwidget( + edit=b, on_activate_call=lambda: self.show_categories_window(source=b)), else: - self.category_selection_button = ba.buttonwidget(edit=self.category_selection_button, - label=label) + b = self.category_selection_button + bui.buttonwidget( + edit=b, + label=label + ) if b.exists() else None + + async def _on_order_button_press(self) -> None: + self.selected_alphabet_order = ('a_z' if self.selected_alphabet_order == 'z_a' else 'z_a') + bui.buttonwidget(edit=self.alphabet_order_selection_button, + label=('Z - A' if self.selected_alphabet_order == 'z_a' else 'A - Z') + ) + filter_text = bui.textwidget(parent=self._root_widget, query=self._filter_widget) + if self.plugin_manager.categories != {}: + if self.plugin_manager.categories['All'] is not None: + await self.draw_plugin_names( + self.selected_category, search_term=filter_text, refresh=True, order=self.selected_alphabet_order + ) def draw_search_bar(self): - search_bar_pos_x = (85 if _uiscale is ba.UIScale.SMALL else - 65 if _uiscale is ba.UIScale.MEDIUM else 90) + search_bar_pos_x = (85 if _uiscale() is babase.UIScale.SMALL else + 68 if _uiscale() is babase.UIScale.MEDIUM else 75) search_bar_pos_y = self._height - ( - 145 if _uiscale is ba.UIScale.SMALL else - 110 if _uiscale is ba.UIScale.MEDIUM else 116) + 145 if _uiscale() is babase.UIScale.SMALL else + 110 if _uiscale() is babase.UIScale.MEDIUM else 116) - search_bar_size_x = (250 if _uiscale is ba.UIScale.SMALL else - 230 if _uiscale is ba.UIScale.MEDIUM else 260) + search_bar_size_x = (320 if _uiscale() is babase.UIScale.SMALL else + 230 if _uiscale() is babase.UIScale.MEDIUM else 260) search_bar_size_y = ( - 35 if _uiscale is ba.UIScale.SMALL else - 35 if _uiscale is ba.UIScale.MEDIUM else 45) - - filter_txt_pos_x = (60 if _uiscale is ba.UIScale.SMALL else - 40 if _uiscale is ba.UIScale.MEDIUM else 60) - filter_txt_pos_y = search_bar_pos_y + (5 if _uiscale is ba.UIScale.SMALL else - 4 if _uiscale is ba.UIScale.MEDIUM else 8) - - ba.textwidget(parent=self._root_widget, - text="Filter", - position=(filter_txt_pos_x, filter_txt_pos_y), - selectable=False, - h_align='left', - v_align='center', - color=ba.app.ui.title_color, - scale=0.5) - - filter_txt = ba.Lstr(resource='filterText') - self._filter_widget = ba.textwidget(parent=self._root_widget, - text="", - size=(search_bar_size_x, search_bar_size_y), - position=(search_bar_pos_x, search_bar_pos_y), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - description=filter_txt) - self._last_filter_text = None + 35 if _uiscale() is babase.UIScale.SMALL else + 35 if _uiscale() is babase.UIScale.MEDIUM else 45) + + filter_txt_pos_x = (60 if _uiscale() is babase.UIScale.SMALL else + 40 if _uiscale() is babase.UIScale.MEDIUM else 50) + filter_txt_pos_y = search_bar_pos_y + (3 if _uiscale() is babase.UIScale.SMALL else + 4 if _uiscale() is babase.UIScale.MEDIUM else 8) + + bui.textwidget(parent=self._root_widget, + text="Filter", + position=(filter_txt_pos_x, filter_txt_pos_y), + selectable=False, + h_align='left', + v_align='center', + color=bui.app.ui_v1.title_color, + scale=0.5) + + filter_txt = babase.Lstr(resource='filterText') + search_bar_maxwidth = search_bar_size_x - (95 if _uiscale() is babase.UIScale.SMALL else + 77 if _uiscale() is babase.UIScale.MEDIUM else + 85) + self._filter_widget = bui.textwidget( + parent=self._root_widget, + text="", + size=(search_bar_size_x, search_bar_size_y), + position=(search_bar_pos_x, search_bar_pos_y), + h_align='left', + v_align='center', + editable=True, + scale=0.8, + autoselect=True, + maxwidth=search_bar_maxwidth, + description=filter_txt + ) + self._last_filter_text = "" self._last_filter_plugins = [] - loop = asyncio.get_event_loop() - loop.create_task(self.process_search_filter()) - async def process_search_filter(self): + loop.create_task(self.process_search_term()) + + async def process_search_term(self): while True: await asyncio.sleep(0.2) - try: - filter_text = ba.textwidget(query=self._filter_widget) - except RuntimeError: + if not self._filter_widget: # Search filter widget got destroyed. No point checking for filter text anymore. return + filter_text = bui.textwidget(parent=self._root_widget, query=self._filter_widget) if self.selected_category is None: continue try: - await self.draw_plugin_names(self.selected_category, search_filter=filter_text) - except (KeyError, AttributeError): - # TODO: Raise a more fitting exception here. Selected category doesn't exist, such - # as the case where refresh button has been tapped on. + await self.draw_plugin_names( + self.selected_category, search_term=filter_text.lower(), order=self.selected_alphabet_order) + except CategoryDoesNotExist: pass - # XXX: This may be more efficient, but we need a way to get a plugin's textwidget - # attributes like color, position and more. - # for plugin in self._columnwidget.get_children(): - # for name, widget in tuple(self.plugins_in_current_view.items()): - # # print(ba.textwidget(query=plugin)) - # # plugin.delete() - # print(dir(widget)) - # if filter_text in name: - # import random - # if random.random() > 0.9: - # ba.textwidget(edit=widget).delete() - # else: - # ba.textwidget(edit=widget, position=None) - # else: - # ba.textwidget(edit=widget, position=None) def draw_settings_icon(self): - settings_pos_x = (500 if _uiscale is ba.UIScale.SMALL else - 440 if _uiscale is ba.UIScale.MEDIUM else 510) - settings_pos_y = (130 if _uiscale is ba.UIScale.SMALL else - 60 if _uiscale is ba.UIScale.MEDIUM else 70) - controller_button = ba.buttonwidget(parent=self._root_widget, - # autoselect=True, - position=(settings_pos_x, settings_pos_y), - size=(30, 30), - button_type="square", - label="", - on_activate_call=ba.Call(PluginManagerSettingsWindow, - self.plugin_manager, - self._root_widget)) - ba.imagewidget(parent=self._root_widget, - position=(settings_pos_x, settings_pos_y), - size=(30, 30), - color=(0.8, 0.95, 1), - texture=ba.gettexture("settingsIcon"), - draw_controller=controller_button) + settings_pos_x = (610 if _uiscale() is babase.UIScale.SMALL else + 500 if _uiscale() is babase.UIScale.MEDIUM else 510) + settings_pos_y = (130 if _uiscale() is babase.UIScale.SMALL else + 60 if _uiscale() is babase.UIScale.MEDIUM else 70) + controller_button = bui.buttonwidget( + parent=self._root_widget, + position=(settings_pos_x, settings_pos_y), + size=(30, 30), + button_type="square", + label="" + ) + bui.buttonwidget( + controller_button, + on_activate_call=babase.Call( + PluginManagerSettingsWindow, + self.plugin_manager, + controller_button + ) + ) + bui.imagewidget( + parent=self._root_widget, + position=(settings_pos_x, settings_pos_y), + size=(30, 30), + color=(0.8, 0.95, 1), + texture=bui.gettexture("settingsIcon"), + draw_controller=controller_button + ) def draw_refresh_icon(self): - settings_pos_x = (500 if _uiscale is ba.UIScale.SMALL else - 440 if _uiscale is ba.UIScale.MEDIUM else 510) - settings_pos_y = (180 if _uiscale is ba.UIScale.SMALL else - 105 if _uiscale is ba.UIScale.MEDIUM else 120) - loop = asyncio.get_event_loop() - controller_button = ba.buttonwidget(parent=self._root_widget, - # autoselect=True, - position=(settings_pos_x, settings_pos_y), - size=(30, 30), - button_type="square", - label="", - on_activate_call=lambda: - loop.create_task(self.refresh())) - ba.imagewidget(parent=self._root_widget, - position=(settings_pos_x, settings_pos_y), - size=(30, 30), - color=(0.8, 0.95, 1), - texture=ba.gettexture("replayIcon"), - draw_controller=controller_button) - - # async def draw_plugin_names(self, category): - # for plugin in self._columnwidget.get_children(): - # plugin.delete() - - # plugins = await self.plugin_manager.categories[category].get_plugins() - # plugin_names_to_draw = tuple(self.draw_plugin_name(plugin) for plugin in plugins) - # await asyncio.gather(*plugin_names_to_draw) + refresh_pos_x = (610 if _uiscale() is babase.UIScale.SMALL else + 500 if _uiscale() is babase.UIScale.MEDIUM else 510) + refresh_pos_y = (180 if _uiscale() is babase.UIScale.SMALL else + 108 if _uiscale() is babase.UIScale.MEDIUM else 120) + + controller_button = bui.buttonwidget( + parent=self._root_widget, + position=(refresh_pos_x, refresh_pos_y), + size=(30, 30), + button_type="square", + label="", + on_activate_call=lambda: loop.create_task(self.refresh()) + ) + bui.imagewidget( + parent=self._root_widget, + position=(refresh_pos_x, refresh_pos_y), + size=(30, 30), + color=(0.8, 0.95, 1), + texture=bui.gettexture("replayIcon"), + draw_controller=controller_button + ) + + def search_term_filterer(self, plugin, search_term): + # This helps resolve "plugin name" to "plugin_name". + if search_term in plugin.info["description"].lower(): + return True + search_term = search_term.replace(" ", "_") + if search_term in plugin.name: + return True + for author in plugin.info["authors"]: + if search_term in author["name"].lower(): + return True + return False # XXX: Not sure if this is the best way to handle search filters. - async def draw_plugin_names(self, category, search_filter=""): - to_draw_plugin_names = (search_filter, category) != (self._last_filter_text, - self.selected_category) - if not to_draw_plugin_names: + async def draw_plugin_names(self, category, search_term="", refresh=False, order='a_z'): + # Re-draw plugin list UI if either search term or category was switched. + to_draw_plugin_names = (search_term, category) != (self._last_filter_text, + self.selected_category) + if not (to_draw_plugin_names or refresh): return - category_plugins = await self.plugin_manager.categories[category].get_plugins() + try: + if self.plugin_manager.categories != {}: + if self.plugin_manager.categories['All'] is not None: + category_plugins = await self.plugin_manager.categories[category if category != 'Installed' else 'All'].get_plugins() + else: + return + else: + return + except (KeyError, AttributeError): + no_internet_text = "Make sure you are connected\n to the Internet and try again." + if bui.textwidget(query=self._plugin_manager_status_text) != no_internet_text: + raise CategoryDoesNotExist(f"{category} does not exist.") + else: + return - if search_filter: - plugins = [] - for plugin in category_plugins: - if search_filter in plugin.name: - plugins.append(plugin) + if search_term: + plugins = list(filter( + lambda plugin: self.search_term_filterer(plugin, search_term), + category_plugins, + )) else: plugins = category_plugins - if plugins == self._last_filter_plugins: + def return_name(val): + return val.name + plugins.sort(key=return_name, reverse=(True if order == 'z_a' else False)) + + if plugins == self._last_filter_plugins and not refresh: + # Plugins names to draw on UI are already drawn. return - self._last_filter_text = search_filter + self._last_filter_text = search_term self._last_filter_plugins = plugins - plugin_names_to_draw = tuple(self.draw_plugin_name(plugin) for plugin in plugins) + if not self._columnwidget.exists(): + return - for plugin in self._columnwidget.get_children(): - plugin.delete() + if category == 'Installed': + plugin_names_to_draw = tuple( + plugin for plugin in plugins if plugin.is_installed + ) + else: + plugin_names_to_draw = plugins + + [plugin.delete() for plugin in self._columnwidget.get_children()] + text_widget = bui.textwidget(parent=self._columnwidget) + text_widget.delete() + # await asyncio.gather(*plugin_names_to_draw) + + plugin_names_ready_to_draw = [] + for plugin in plugin_names_to_draw: + try: + lcv = plugin.latest_compatible_version + except NoCompatibleVersion: + continue + plugin_names_ready_to_draw += [plugin] + + for i, plugin in enumerate(plugin_names_ready_to_draw): + await self.draw_plugin_name(plugin, plugin_names_ready_to_draw) - await asyncio.gather(*plugin_names_to_draw) + async def draw_plugin_name(self, plugin, plugins_list): - async def draw_plugin_name(self, plugin): if plugin.is_installed: local_plugin = plugin.get_local() if await local_plugin.is_enabled(): if not local_plugin.is_installed_via_plugin_manager: color = (0.8, 0.2, 0.2) elif local_plugin.version == plugin.latest_compatible_version.number: - color = (0, 1, 0) + color = (0, 0.95, 0.2) else: color = (1, 0.6, 0) else: @@ -1447,41 +2315,49 @@ async def draw_plugin_name(self, plugin): plugin_name_widget_to_update = self.plugins_in_current_view.get(plugin.name) if plugin_name_widget_to_update: - ba.textwidget(edit=plugin_name_widget_to_update, - color=color) + bui.textwidget( + edit=plugin_name_widget_to_update, + color=color + ) else: - text_widget = ba.textwidget(parent=self._columnwidget, - size=(410, 30), - selectable=True, - always_highlight=True, - color=color, - # on_select_call=lambda: None, - text=plugin.name, - click_activate=True, - on_activate_call=lambda: self.show_plugin_window(plugin), - h_align='left', - v_align='center', - maxwidth=420) + text_widget = bui.textwidget( + parent=self._columnwidget, + size=(410, 30), + selectable=True, + always_highlight=True, + color=color, + text=plugin.name.replace('_', ' ').title(), + click_activate=True, + on_activate_call=lambda: self.show_plugin_window(plugin, plugins_list), + h_align='left', + v_align='center', + maxwidth=420 + ) self.plugins_in_current_view[plugin.name] = text_widget # XXX: This seems nicer. Might wanna use this in future. # text_widget.add_delete_callback(lambda: self.plugins_in_current_view.pop(plugin.name)) - def show_plugin_window(self, plugin): - PluginWindow(plugin, self._root_widget, lambda: self.draw_plugin_name(plugin)) + def show_plugin_window(self, plugin, plugins_list): + PluginWindow( + plugin, + self._root_widget, + plugins_list=plugins_list, + button_callback=lambda: self.draw_plugin_name(plugin, plugins_list) + ) - def show_categories_window(self): - play_sound() + def show_categories_window(self, source): PluginCategoryWindow( self.plugin_manager.categories.keys(), self.selected_category, - self._root_widget, + source, self.select_category, ) async def select_category(self, category): self.plugins_in_current_view.clear() self.draw_category_selection_button(post_label=category) - await self.draw_plugin_names(category, search_filter=self._last_filter_text) + await self.draw_plugin_names( + category, search_term=self._last_filter_text, refresh=True, order=self.selected_alphabet_order) self.selected_category = category def cleanup(self): @@ -1489,20 +2365,27 @@ def cleanup(self): for plugin in self._columnwidget.get_children(): plugin.delete() self.plugins_in_current_view.clear() - self._last_filter_text = None + self._last_filter_text = "" self._last_filter_plugins = [] async def refresh(self): - play_sound() self.cleanup() - ba.textwidget(edit=self._plugin_manager_status_text, - text="Refreshing...") + # try: + # bui.textwidget(edit=self._plugin_manager_status_text, text="Refreshing") + # except: + # pass + + self.spin(True) with self.exception_handler(): await self.plugin_manager.refresh() + await self.plugin_manager.setup_changelog() await self.plugin_manager.setup_index() - ba.textwidget(edit=self._plugin_manager_status_text, - text="") + self.spin() + try: + bui.textwidget(edit=self._plugin_manager_status_text, text="") + except: + pass await self.select_category(self.selected_category) def soft_refresh(self): @@ -1511,91 +2394,164 @@ def soft_refresh(self): class PluginManagerSettingsWindow(popup.PopupWindow): def __init__(self, plugin_manager, origin_widget): - play_sound() self._plugin_manager = plugin_manager self.scale_origin = origin_widget.get_screen_space_center() - self.settings = ba.app.config["Community Plugin Manager"]["Settings"].copy() - loop = asyncio.get_event_loop() + self.settings = babase.app.config["Community Plugin Manager"]["Settings"].copy() + loop.create_task(self.draw_ui()) async def draw_ui(self): - b_text_color = (0.75, 0.7, 0.8) - s = 1.1 if _uiscale is ba.UIScale.SMALL else 1.27 if ba.UIScale.MEDIUM else 1.57 + b_text_color = (0.8, 0.8, 0.85) + s = 1.25 if _uiscale() is babase.UIScale.SMALL else 1.27 if _uiscale() is babase.UIScale.MEDIUM else 1.3 width = 380 * s height = 150 + 150 * s color = (0.9, 0.9, 0.9) + + # Subtracting the default bluish-purple color from the texture, so it's as close + # as to white as possible. + discord_fg_color = (10 - 0.32, 10 - 0.39, 10 - 0.96) + discord_bg_color = (0.525, 0.595, 1.458) + github_bg_color = (0.23, 0.23, 0.23) text_scale = 0.7 * s self._transition_out = 'out_scale' transition = 'in_scale' - button_size = (60 * s, 32 * s) + button_size = (32 * s, 32 * s) # index = await self._plugin_manager.get_index() - self._root_widget = ba.containerwidget(size=(width, height), - # parent=_ba.get_special_widget( - # 'overlay_stack'), - on_outside_click_call=self._ok, - transition=transition, - scale=(2.1 if _uiscale is ba.UIScale.SMALL else 1.5 - if _uiscale is ba.UIScale.MEDIUM else 1.0), - scale_origin_stack_offset=self.scale_origin) + self._root_widget = bui.containerwidget( + size=(width, height), + on_outside_click_call=self._ok, + transition=transition, + scale=(2.1 if _uiscale() is babase.UIScale.SMALL else 1.5 + if _uiscale() is babase.UIScale.MEDIUM else 1.0), + scale_origin_stack_offset=self.scale_origin + ) + _add_popup(self) pos = height * 0.9 setting_title = "Settings" - ba.textwidget(parent=self._root_widget, - position=(width * 0.49, pos), - size=(0, 0), - h_align='center', - v_align='center', - text=setting_title, - scale=text_scale, - color=ba.app.ui.title_color, - maxwidth=width * 0.9) + bui.textwidget( + parent=self._root_widget, + position=(width * 0.49, pos), + size=(0, 0), + h_align='center', + v_align='center', + text=setting_title, + scale=text_scale, + color=bui.app.ui_v1.title_color, + maxwidth=width * 0.9 + ) pos -= 20 - self._save_button = ba.buttonwidget(parent=self._root_widget, - position=((width * 0.82) - button_size[0] / 2, pos), - size=(73, 35), - on_activate_call=self.save_settings_button, - textcolor=b_text_color, - button_type='square', - text_scale=1, - scale=0, - selectable=False, - label="Save") + self._changelog_button = b = bui.buttonwidget( + parent=self._root_widget, + position=((width * 0.2) - button_size[0] / 2 - 5, pos), + size=(80, 30), + textcolor=b_text_color, + button_type='square', + label='' + ) + bui.buttonwidget(b, on_activate_call=lambda: ChangelogWindow(b)) + bui.textwidget( + parent=self._root_widget, + position=((width * 0.2) - button_size[0] / 2, pos), + size=(70, 30), + scale=0.6, + h_align='center', + v_align='center', + text='ChangeLog', + color=b_text_color, + draw_controller=self._changelog_button, + ) + self._save_button = bui.buttonwidget( + parent=self._root_widget, + position=((width * 0.82) - button_size[0] / 2, pos), + size=(73, 35), + on_activate_call=self.save_settings_button, + textcolor=b_text_color, + button_type='square', + text_scale=1, + scale=0, + selectable=False, + label="Save" + ) pos -= 40 for setting, value in self.settings.items(): - ba.checkboxwidget(parent=self._root_widget, - position=(width * 0.1, pos), - size=(170, 30), - text=setting, - value=value, - on_value_change_call=ba.Call(self.toggle_setting, setting), - maxwidth=500, - textcolor=(0.9, 0.9, 0.9), - scale=0.75) - pos -= 32 + bui.checkboxwidget( + parent=self._root_widget, + position=(width * 0.1, pos), + size=(170, 30), + text=setting, + value=value, + on_value_change_call=babase.Call(self.toggle_setting, setting), + maxwidth=500, + textcolor=(0.9, 0.9, 0.9), + scale=text_scale * 0.8 + ) + pos -= 34 * text_scale - pos -= 20 - ba.textwidget(parent=self._root_widget, - position=(width * 0.49, pos-5), - size=(0, 0), - h_align='center', - v_align='center', - text='Contribute to plugins or to this community plugin manager!', - scale=text_scale * 0.65, - color=color, - maxwidth=width * 0.95) - - pos -= 70 - ba.buttonwidget(parent=self._root_widget, - position=((width * 0.49) - button_size[0] / 2, pos), - size=button_size, - on_activate_call=lambda: ba.open_url(REPOSITORY_URL), - textcolor=b_text_color, - button_type='square', - text_scale=1, - label='GitHub') - ba.containerwidget(edit=self._root_widget, - on_cancel_call=self._ok) + pos = height - 200 + bui.textwidget( + parent=self._root_widget, + position=(width * 0.49, pos-5), + size=(0, 0), + h_align='center', + v_align='center', + text='Contribute to plugins or to this community plugin manager!', + scale=text_scale * 0.65, + color=color, + maxwidth=width * 0.95 + ) + + pos -= 75 + try: + plugin_manager_update_available = await self._plugin_manager.get_update_details() + except urllib.error.URLError: + plugin_manager_update_available = False + discord_width = (width * 0.20) if plugin_manager_update_available else (width * 0.31) + self.discord_button = bui.buttonwidget( + parent=self._root_widget, + position=(discord_width - button_size[0] / 2, pos), + size=button_size, + on_activate_call=lambda: bui.open_url(DISCORD_URL), + textcolor=b_text_color, + color=discord_bg_color, + button_type='square', + text_scale=1, + label="" + ) + + bui.imagewidget( + parent=self._root_widget, + position=(discord_width+0.5 - button_size[0] / 2, pos), + size=button_size, + texture=bui.gettexture("discordLogo"), + color=discord_fg_color, + draw_controller=self.discord_button + ) + + github_width = (width * 0.49) if plugin_manager_update_available else (width * 0.65) + self.github_button = bui.buttonwidget( + parent=self._root_widget, + position=(github_width - button_size[0] / 2, pos), + size=button_size, + on_activate_call=lambda: bui.open_url(REPOSITORY_URL), + textcolor=b_text_color, + color=github_bg_color, + button_type='square', + text_scale=1, + label='' + ) + + bui.imagewidget( + parent=self._root_widget, + position=(github_width + 0.5 - button_size[0] / 2, pos), + size=button_size, + texture=bui.gettexture("githubLogo"), + color=(1, 1, 1), + draw_controller=self.github_button + ) + + bui.containerwidget(edit=self._root_widget, on_cancel_call=self._ok) try: plugin_manager_update_available = await self._plugin_manager.get_update_details() @@ -1603,394 +2559,367 @@ async def draw_ui(self): plugin_manager_update_available = False if plugin_manager_update_available: text_color = (0.75, 0.2, 0.2) - loop = asyncio.get_event_loop() button_size = (95 * s, 32 * s) update_button_label = f'Update to v{plugin_manager_update_available[0]}' - self._update_button = ba.buttonwidget(parent=self._root_widget, - position=((width * 0.77) - button_size[0] / 2, - pos), - size=button_size, - on_activate_call=lambda: - loop.create_task( - self.update( - *plugin_manager_update_available - ) - ), - textcolor=b_text_color, - button_type='square', - text_scale=1, - color=(0, 0.7, 0), - label=update_button_label) - self._restart_to_reload_changes_text = ba.textwidget(parent=self._root_widget, - position=(width * 0.79, pos + 20), - size=(0, 0), - h_align='center', - v_align='center', - text='', - scale=text_scale * 0.65, - color=(0, 0.8, 0), - maxwidth=width * 0.9) + self._update_button = bui.buttonwidget( + parent=self._root_widget, + position=((width * 0.77) - button_size[0] / 2, pos), + size=button_size, + on_activate_call=lambda: loop.create_task( + self.update(*plugin_manager_update_available)), + textcolor=b_text_color, + button_type='square', + text_scale=1, + color=(0, 0.7, 0), + label=update_button_label + ) + self._restart_to_reload_changes_text = bui.textwidget( + parent=self._root_widget, + position=(width * 0.79, pos + 20), + size=(0, 0), + h_align='center', + v_align='center', + text='', + scale=text_scale * 0.65, + color=(0, 0.8, 0), + maxwidth=width * 0.9 + ) else: text_color = (0, 0.8, 0) pos -= 25 - ba.textwidget(parent=self._root_widget, - position=(width * 0.49, pos), - size=(0, 0), - h_align='center', - v_align='center', - text=f'Plugin Manager v{PLUGIN_MANAGER_VERSION}', - scale=text_scale * 0.8, - color=text_color, - maxwidth=width * 0.9) + bui.textwidget( + parent=self._root_widget, + position=(width * 0.49, pos), + size=(0, 0), + h_align='center', + v_align='center', + text=f'Plugin Manager v{PLUGIN_MANAGER_VERSION}', + scale=text_scale * 0.8, + color=text_color, + maxwidth=width * 0.9 + ) + pos -= 25 + bui.textwidget( + parent=self._root_widget, + position=(width * 0.49, pos), + size=(0, 0), + h_align='center', + v_align='center', + text=f'API Version: {_app_api_version}', + scale=text_scale * 0.7, + color=(0.4, 0.8, 1), + maxwidth=width * 0.95 + ) pos = height * 0.1 def toggle_setting(self, setting, set_value): self.settings[setting] = set_value - if self.settings == ba.app.config["Community Plugin Manager"]["Settings"]: - ba.buttonwidget(edit=self._save_button, - scale=0, - selectable=False) - else: - ba.buttonwidget(edit=self._save_button, - scale=1, - selectable=True) + check = self.settings == babase.app.config["Community Plugin Manager"]["Settings"] + bui.buttonwidget( + edit=self._save_button, + scale=0 if check else 1, + selectable=(not check) + ) def save_settings_button(self): - ba.app.config["Community Plugin Manager"]["Settings"] = self.settings.copy() - ba.app.config.commit() + babase.app.config["Community Plugin Manager"]["Settings"] = self.settings.copy() + babase.app.config.commit() self._ok() + bui.getsound('gunCocking').play() async def update(self, to_version=None, commit_sha=None): try: await self._plugin_manager.update(to_version, commit_sha) - except TypeError: - # TODO: Catch a more fitting exception here. - ba.screenmessage("md5sum check failed", color=(1, 0, 0)) + await self._plugin_manager.setup_changelog() + except MD5CheckSumFailed: + bui.screenmessage("MD5 checksum failed during plugin manager update", color=(1, 0, 0)) + bui.getsound('error').play() else: - ba.screenmessage("Update successful.", color=(0, 1, 0)) - ba.textwidget(edit=self._restart_to_reload_changes_text, - text='Update Applied!\nRestart game to reload changes.') + bui.screenmessage("Plugin manager update successful", color=(0, 1, 0)) + bui.getsound('shieldUp').play() + bui.textwidget( + edit=self._restart_to_reload_changes_text, + text='Update Applied!\nRestart game to reload changes.' + ) self._update_button.delete() def _ok(self) -> None: - play_sound() - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.getsound('swish').play() + _remove_popup(self) + bui.containerwidget(edit=self._root_widget, transition='out_scale') + +class NewAllSettingsWindow(AllSettingsWindow): + """Window for selecting a settings category.""" -class NewAllSettingsWindow(ba.Window): - def __init__(self, - transition: str = "in_right", - origin_widget: ba.Widget = None): + def __init__( + self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals - import threading + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + width = 1000 if uiscale is bui.UIScale.SMALL else 800 + x_inset = 125 if uiscale is bui.UIScale.SMALL else 105 + height = 490 + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 + self._plugman_button = None - # Preload some modules we use in a background thread so we won"t - # have a visual hitch when the user taps them. - threading.Thread(target=self._preload_modules).start() + super().__init__(transition, origin_widget) - ba.set_analytics_screen("Settings Window") - scale_origin: Optional[tuple[float, float]] - if origin_widget is not None: - self._transition_out = "out_scale" - scale_origin = origin_widget.get_screen_space_center() - transition = "in_scale" - else: - self._transition_out = "out_right" - scale_origin = None - width = 900 if _uiscale is ba.UIScale.SMALL else 670 - x_inset = 75 if _uiscale is ba.UIScale.SMALL else 0 - height = 435 - self._r = "settingsWindow" - top_extra = 20 if _uiscale is ba.UIScale.SMALL else 0 - - super().__init__(root_widget=ba.containerwidget( - size=(width, height + top_extra), - transition=transition, - toolbar_visibility="menu_minimal", - scale_origin_stack_offset=scale_origin, - scale=(1.75 if _uiscale is ba.UIScale.SMALL else - 1.35 if _uiscale is ba.UIScale.MEDIUM else 1.0), - stack_offset=(0, -8) if _uiscale is ba.UIScale.SMALL else (0, 0))) + for child in self._root_widget.get_children(): + child.delete() + + bui.containerwidget( + edit=self._root_widget, size=(width, height + top_extra) + ) - if ba.app.ui.use_toolbars and _uiscale is ba.UIScale.SMALL: + if uiscale is bui.UIScale.SMALL: self._back_button = None - ba.containerwidget(edit=self._root_widget, - on_cancel_call=self._do_back) + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self.main_window_back + ) else: - self._back_button = btn = ba.buttonwidget( + self._back_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, - position=(40 + x_inset, height - 55), + position=(x_inset - 20, height - 85), size=(130, 60), scale=0.8, text_scale=1.2, - label=ba.Lstr(resource="backText"), - button_type="back", - on_activate_call=self._do_back) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) - - ba.textwidget(parent=self._root_widget, - position=(0, height - 44), - size=(width, 25), - text=ba.Lstr(resource=self._r + ".titleText"), - color=ba.app.ui.title_color, - h_align="center", - v_align="center", - maxwidth=130) - - if self._back_button is not None: - ba.buttonwidget(edit=self._back_button, - button_type="backSmall", - size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK)) + label=bui.Lstr(resource='backText'), + button_type='back', + on_activate_call=self.main_window_back, + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) - v = height - 80 - v -= 145 + bui.textwidget( + parent=self._root_widget, + position=(0, height - 80), + size=(width, 25), + text=bui.Lstr(resource=f'{self._r}.titleText'), + color=bui.app.ui_v1.title_color, + h_align='center', + v_align='center', + maxwidth=130, + ) - basew = 200 - baseh = 160 + if self._back_button is not None: + bui.buttonwidget( + edit=self._back_button, + button_type='backSmall', + size=(60, 60), + label=bui.charstr(bui.SpecialChar.BACK), + ) - x_offs = x_inset + (105 if _uiscale is ba.UIScale.SMALL else - 72) - basew # now unused + v = height - 265 + basew = 280 if uiscale is bui.UIScale.SMALL else 230 + baseh = 170 + x_offs = ( + x_inset + (105 if uiscale is bui.UIScale.SMALL else 72) - basew + ) # now unused + x_dif = (basew - 7) / 2 x_offs2 = x_offs + basew - 7 x_offs3 = x_offs + 2 * (basew - 7) x_offs4 = x_offs + 3 * (basew - 7) - x_offs5 = x_offs2 + 0.5 * (basew - 7) - x_offs6 = x_offs5 + (basew - 7) - - def _b_title(x: float, y: float, button: ba.Widget, - text: Union[str, ba.Lstr]) -> None: - ba.textwidget(parent=self._root_widget, - text=text, - position=(x + basew * 0.47, y + baseh * 0.22), - maxwidth=basew * 0.7, size=(0, 0), - h_align="center", - v_align="center", - draw_controller=button, - color=(0.7, 0.9, 0.7, 1.0)) - - ctb = self._controllers_button = ba.buttonwidget(parent=self._root_widget, - autoselect=True, - position=(x_offs2, v), - size=(basew, baseh), - button_type="square", - label="", - on_activate_call=self._do_controllers) - if ba.app.ui.use_toolbars and self._back_button is None: - bbtn = _ba.get_special_widget("back_button") - ba.widget(edit=ctb, left_widget=bbtn) - _b_title(x_offs2, v, ctb, - ba.Lstr(resource=self._r + ".controllersText")) + x_offs5 = x_offs2 + x_offs6 = x_offs3 + x_offs2 -= x_dif + x_offs3 -= x_dif + x_offs4 -= x_dif + + def _b_title( + x: float, y: float, button: bui.Widget, text: str | bui.Lstr + ) -> None: + bui.textwidget( + parent=self._root_widget, + text=text, + position=(x + basew * 0.47, y + baseh * 0.22), + maxwidth=basew * 0.7, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=button, + color=(0.7, 0.9, 0.7, 1.0), + ) + + ctb = self._controllers_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(x_offs2, v), + size=(basew, baseh), + button_type='square', + label='', + on_activate_call=self._do_controllers, + ) + if self._back_button is None: + bbtn = bui.get_special_widget('back_button') + bui.widget(edit=ctb, left_widget=bbtn) + _b_title( + x_offs2, v, ctb, bui.Lstr(resource=f'{self._r}.controllersText') + ) imgw = imgh = 130 - ba.imagewidget(parent=self._root_widget, - position=(x_offs2 + basew * 0.49 - imgw * 0.5, v + 35), - size=(imgw, imgh), - texture=ba.gettexture("controllerIcon"), - draw_controller=ctb) - - gfxb = self._graphics_button = ba.buttonwidget(parent=self._root_widget, - autoselect=True, - position=(x_offs3, v), - size=(basew, baseh), - button_type="square", - label="", - on_activate_call=self._do_graphics) - if ba.app.ui.use_toolbars: - pbtn = _ba.get_special_widget("party_button") - ba.widget(edit=gfxb, up_widget=pbtn, right_widget=pbtn) - _b_title(x_offs3, v, gfxb, ba.Lstr(resource=self._r + ".graphicsText")) + bui.imagewidget( + parent=self._root_widget, + position=(x_offs2 + basew * 0.49 - imgw * 0.5, v + 35), + size=(imgw, imgh), + texture=bui.gettexture('controllerIcon'), + draw_controller=ctb, + ) + + gfxb = self._graphics_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(x_offs3, v), + size=(basew, baseh), + button_type='square', + label='', + on_activate_call=self._do_graphics, + ) + pbtn = bui.get_special_widget('squad_button') + bui.widget(edit=gfxb, up_widget=pbtn, right_widget=pbtn) + _b_title(x_offs3, v, gfxb, bui.Lstr(resource=f'{self._r}.graphicsText')) imgw = imgh = 110 - ba.imagewidget(parent=self._root_widget, - position=(x_offs3 + basew * 0.49 - imgw * 0.5, v + 42), - size=(imgw, imgh), - texture=ba.gettexture("graphicsIcon"), - draw_controller=gfxb) - - abtn = self._audio_button = ba.buttonwidget(parent=self._root_widget, - autoselect=True, - position=(x_offs4, v), - size=(basew, baseh), - button_type="square", - label="", - on_activate_call=self._do_audio) - _b_title(x_offs4, v, abtn, ba.Lstr(resource=self._r + ".audioText")) - imgw = imgh = 120 - ba.imagewidget(parent=self._root_widget, - position=(x_offs4 + basew * 0.49 - imgw * 0.5 + 5, v + 35), - size=(imgw, imgh), - color=(1, 1, 0), texture=ba.gettexture("audioIcon"), - draw_controller=abtn) - v -= (baseh - 5) - - avb = self._advanced_button = ba.buttonwidget(parent=self._root_widget, - autoselect=True, - position=(x_offs5, v), - size=(basew, baseh), - button_type="square", - label="", - on_activate_call=self._do_advanced) - _b_title(x_offs5, v, avb, ba.Lstr(resource=self._r + ".advancedText")) + bui.imagewidget( + parent=self._root_widget, + position=(x_offs3 + basew * 0.49 - imgw * 0.5, v + 42), + size=(imgw, imgh), + texture=bui.gettexture('graphicsIcon'), + draw_controller=gfxb, + ) + + abtn = self._audio_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(x_offs4, v), + size=(basew, baseh), + button_type='square', + label='', + on_activate_call=self._do_audio, + ) + _b_title(x_offs4, v, abtn, bui.Lstr(resource=f'{self._r}.audioText')) imgw = imgh = 120 - ba.imagewidget(parent=self._root_widget, - position=(x_offs5 + basew * 0.49 - imgw * 0.5 + 5, - v + 35), - size=(imgw, imgh), - color=(0.8, 0.95, 1), - texture=ba.gettexture("advancedIcon"), - draw_controller=avb) - - mmb = self._modmgr_button = ba.buttonwidget(parent=self._root_widget, - autoselect=True, - position=(x_offs6, v), - size=(basew, baseh), - button_type="square", - label="", - on_activate_call=self._do_modmanager) - _b_title(x_offs6, v, avb, ba.Lstr(value="Plugin Manager")) + bui.imagewidget( + parent=self._root_widget, + position=(x_offs4 + basew * 0.49 - imgw * 0.5 + 5, v + 35), + size=(imgw, imgh), + color=(1, 1, 0), + texture=bui.gettexture('audioIcon'), + draw_controller=abtn, + ) + + v -= baseh - 5 + + avb = self._advanced_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(x_offs5, v), + size=(basew, baseh), + button_type='square', + label='', + on_activate_call=self._do_advanced, + ) + _b_title(x_offs5, v, avb, bui.Lstr(resource=f'{self._r}.advancedText')) imgw = imgh = 120 - ba.imagewidget(parent=self._root_widget, - position=(x_offs6 + basew * 0.49 - imgw * 0.5 + 5, - v + 35), - size=(imgw, imgh), - color=(0.8, 0.95, 1), - texture=ba.gettexture("heart"), - draw_controller=mmb) + bui.imagewidget( + parent=self._root_widget, + position=(x_offs5 + basew * 0.49 - imgw * 0.5 + 5, v + 35), + size=(imgw, imgh), + color=(0.8, 0.95, 1), + texture=bui.gettexture('advancedIcon'), + draw_controller=avb, + ) + self._plugman_button = pmb = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(x_offs6, v), + size=(basew, baseh), + button_type='square', + label='', + on_activate_call=self._do_plugman, + ) + _b_title(x_offs6, v, pmb, bui.Lstr(value="Plugin Manager")) + imgw = imgh = 120 + bui.imagewidget( + parent=self._root_widget, + position=(x_offs6 + basew * 0.49 - imgw * 0.5 + 5, v + 35), + size=(imgw, imgh), + color=(0.8, 0.95, 1), + texture=bui.gettexture('storeIcon'), + draw_controller=pmb, + ) self._restore_state() - # noinspection PyUnresolvedReferences - @staticmethod - def _preload_modules() -> None: - """Preload modules we use (called in bg thread).""" - # import bastd.ui.mainmenu as _unused1 - # import bastd.ui.settings.controls as _unused2 - # import bastd.ui.settings.graphics as _unused3 - # import bastd.ui.settings.audio as _unused4 - # import bastd.ui.settings.advanced as _unused5 - - def _do_back(self) -> None: - # pylint: disable=cyclic-import - from bastd.ui.mainmenu import MainMenuWindow - self._save_state() - ba.containerwidget(edit=self._root_widget, - transition=self._transition_out) - ba.app.ui.set_main_menu_window( - MainMenuWindow(transition="in_left").get_root_widget()) - - def _do_controllers(self) -> None: - # pylint: disable=cyclic-import - from bastd.ui.settings.controls import ControlsSettingsWindow - self._save_state() - ba.containerwidget(edit=self._root_widget, transition="out_left") - ba.app.ui.set_main_menu_window(ControlsSettingsWindow( - origin_widget=self._controllers_button).get_root_widget()) - - def _do_graphics(self) -> None: - # pylint: disable=cyclic-import - from bastd.ui.settings.graphics import GraphicsSettingsWindow - self._save_state() - ba.containerwidget(edit=self._root_widget, transition="out_left") - ba.app.ui.set_main_menu_window(GraphicsSettingsWindow( - origin_widget=self._graphics_button).get_root_widget()) - - def _do_audio(self) -> None: - # pylint: disable=cyclic-import - from bastd.ui.settings.audio import AudioSettingsWindow - self._save_state() - ba.containerwidget(edit=self._root_widget, transition="out_left") - ba.app.ui.set_main_menu_window(AudioSettingsWindow( - origin_widget=self._audio_button).get_root_widget()) - - def _do_advanced(self) -> None: - # pylint: disable=cyclic-import - from bastd.ui.settings.advanced import AdvancedSettingsWindow - self._save_state() - ba.containerwidget(edit=self._root_widget, transition="out_left") - ba.app.ui.set_main_menu_window(AdvancedSettingsWindow( - origin_widget=self._advanced_button).get_root_widget()) - - def _do_modmanager(self) -> None: - self._save_state() - ba.containerwidget(edit=self._root_widget, transition="out_left") - ba.app.ui.set_main_menu_window(PluginManagerWindow( - origin_widget=self._modmgr_button).get_root_widget()) + def _do_plugman(self) -> None: + # no-op if we're not in control. + if not self.main_window_has_control(): + return + + self.main_window_replace( + PluginManagerWindow(origin_widget=self._plugman_button) + ) def _save_state(self) -> None: try: sel = self._root_widget.get_selected_child() if sel == self._controllers_button: - sel_name = "Controllers" + sel_name = 'Controllers' elif sel == self._graphics_button: - sel_name = "Graphics" + sel_name = 'Graphics' elif sel == self._audio_button: - sel_name = "Audio" + sel_name = 'Audio' elif sel == self._advanced_button: - sel_name = "Advanced" - elif sel == self._modmgr_button: - sel_name = "Mod Manager" + sel_name = 'Advanced' + elif sel == self._plugman_button: + sel_name = 'PlugMan' elif sel == self._back_button: - sel_name = "Back" + sel_name = 'Back' else: - raise ValueError(f"unrecognized selection \"{sel}\"") - ba.app.ui.window_states[type(self)] = {"sel_name": sel_name} + raise ValueError(f'unrecognized selection \'{sel}\'') + assert bui.app.classic is not None + bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} except Exception: - ba.print_exception(f"Error saving state for {self}.") + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: try: - sel_name = ba.app.ui.window_states.get(type(self), - {}).get("sel_name") - sel: Optional[ba.Widget] - if sel_name == "Controllers": + assert bui.app.classic is not None + sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( + 'sel_name' + ) + sel: bui.Widget | None + if sel_name == 'Controllers': sel = self._controllers_button - elif sel_name == "Graphics": + elif sel_name == 'Graphics': sel = self._graphics_button - elif sel_name == "Audio": + elif sel_name == 'Audio': sel = self._audio_button - elif sel_name == "Advanced": + elif sel_name == 'Advanced': sel = self._advanced_button - elif sel_name == "Mod Manager": - sel = self._modmgr_button - elif sel_name == "Back": + elif sel_name == "PlugMan": + sel = self._plugman_button + elif sel_name == 'Back': sel = self._back_button else: sel = self._controllers_button if sel is not None: - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: - ba.print_exception(f"Error restoring state for {self}.") + logging.exception('Error restoring state for %s.', self) -# ba_meta export plugin -class EntryPoint(ba.Plugin): +# ba_meta export babase.Plugin +class EntryPoint(babase.Plugin): def on_app_running(self) -> None: """Called when the app is being launched.""" - from bastd.ui.settings import allsettings + from bauiv1lib.settings import allsettings allsettings.AllSettingsWindow = NewAllSettingsWindow - asyncio.set_event_loop(ba._asyncio._asyncio_event_loop) + DNSBlockWorkaround.apply() + asyncio.set_event_loop(babase._asyncio._asyncio_event_loop) startup_tasks = StartupTasks() - loop = asyncio.get_event_loop() + loop.create_task(startup_tasks.execute()) - # loop = asyncio.get_event_loop() - # loop.create_task(do()) - # pm = PluginManager() - # pm.plugin_index() - - def on_app_pause(self) -> None: - """Called after pausing game activity.""" - print("pause") - - def on_app_resume(self) -> None: - """Called after the game continues.""" - print("resume") - - def on_app_shutdown(self) -> None: - """Called before closing the application.""" - print("shutdown") - # print(ba.app.config["Community Plugin Manager"]) - # with open(_env["config_file_path"], "r") as fin: - # c = fin.read() - # import json - # print(json.loads(c)["Community Plugin Manager"]) diff --git a/plugins/maps.json b/plugins/maps.json index 4aa261199..255a4c457 100644 --- a/plugins/maps.json +++ b/plugins/maps.json @@ -1,6 +1,63 @@ { "name": "Maps", "description": "Maps", - "plugins_base_url": "http://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/maps", - "plugins": {} -} + "plugins_base_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/maps", + "plugins": { + "forest": { + "description": "Basically bridgit map but outter version", + "external_url": "", + "authors": [ + { + "name": "Hopeless", + "email": "", + "discord": "hope1ess_." + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "52aea58884e486afea63be6b2411aeb8" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "4ccfc7b", + "released_on": "25-01-2025", + "md5sum": "d9bef02a82056bfc493722a44a4bcd35" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "8e5970b", + "released_on": "18-01-2024", + "md5sum": "738e250e43633bafae357ed4b999864a" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "985e486", + "released_on": "15-06-2023", + "md5sum": "01fb81dc27d63789f31559140ec2bd72" + } + } + }, + "forest_v2": { + "description": "A better looking land with some trees\nNew mini games added so you can play more on this update forest", + "external_url": "", + "authors": [ + { + "name": "Startingbat", + "email": "", + "discord": "startingbat" + } + ], + "versions": { + "1.0.0": { + "api_version": 9, + "commit_sha": "4a20493", + "released_on": "22-01-2026", + "md5sum": "42af7d02a8c0d03ad8aaf6da1cf2bac0" + } + } + } + } +} \ No newline at end of file diff --git a/plugins/maps/forest.py b/plugins/maps/forest.py new file mode 100644 index 000000000..526f3ec20 --- /dev/null +++ b/plugins/maps/forest.py @@ -0,0 +1,122 @@ +# ba_meta require api 9 +from __future__ import annotations +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1 import _map +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.maps import * + +if TYPE_CHECKING: + pass + + +class ForestMapData(): + points = {} + boxes = {} + + boxes['area_of_interest_bounds'] = ( + (0.0, 1.185751251, 0.4326226188) + + (0.0, 0.0, 0.0) + + (29.8180273, 11.57249038, 18.89134176) + ) + boxes['edge_box'] = ( + (-0.103873591, 0.4133341891, 0.4294651013) + + (0.0, 0.0, 0.0) + + (22.48295719, 1.290242794, 8.990252454) + ) + points['ffa_spawn1'] = (-2.0, -2.0, -4.373674593) + ( + 8.895057015, + 1.0, + 0.444350722, + ) + points['ffa_spawn2'] = (-2.0, -2.0, 2.076288941) + ( + 8.895057015, + 1.0, + 0.444350722, + ) + boxes['map_bounds'] = ( + (0.0, 1.185751251, 0.4326226188) + + (0.0, 0.0, 0.0) + + (42.09506485, 22.81173179, 29.76723155) + ) + points['flag_default'] = (-2.5, -3.0, -2.0) + points['powerup_spawn1'] = (-6.0, -2.6, -1.25) + points['powerup_spawn2'] = (1.0, -2.6, -1.25) + points['spawn1'] = (-10.0, -2.0, -2.0) + (0.5, 1.0, 3.2) + points['spawn2'] = (5.0, -2.0, -2.0) + (0.5, 1.0, 3.2) + + +class ForestMap(bs.Map): + + defs = ForestMapData() + name = 'Forest' + + @classmethod + def get_play_types(cls) -> list[str]: + return ['melee', 'keep_away'] + + @classmethod + def get_preview_texture_name(cls) -> list[str]: + return 'natureBackgroundColor' + + @classmethod + def on_preload(cls) -> any: + data: dict[str, any] = { + 'mesh': bs.getmesh('natureBackground'), + 'tex': bs.gettexture('natureBackgroundColor'), + 'collision_mesh': bs.getcollisionmesh('natureBackgroundCollide'), + 'bgmesh': bs.getmesh('thePadBG'), + 'bgtex': bs.gettexture('menuBG') + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + self.node = bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': self.preloaddata['mesh'], + 'color_texture': self.preloaddata['tex'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'materials': [shared.footing_material] + } + ) + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + # 'shadow': True, + 'color_texture': self.preloaddata['bgtex'] + } + ) + + gnode = bs.getactivity().globalsnode + gnode.tint = (1.0, 1.10, 1.15) + gnode.ambient_color = (0.9, 1.3, 1.1) + gnode.shadow_ortho = False + gnode.vignette_outer = (0.76, 0.76, 0.76) + gnode.vignette_inner = (0.95, 0.95, 0.99) + + def is_point_near_edge(self, + point: babase.Vec3, + running: bool = False) -> bool: + xpos = point.x + zpos = point.z + x_adj = xpos * 0.125 + z_adj = (zpos + 3.7) * 0.2 + if running: + x_adj *= 1.4 + z_adj *= 1.4 + return x_adj * x_adj + z_adj * z_adj > 1.0 + + +# ba_meta export babase.Plugin +class EnableMe(babase.Plugin): + _map.register_map(ForestMap) diff --git a/plugins/maps/forest_v2.py b/plugins/maps/forest_v2.py new file mode 100644 index 000000000..48f794484 --- /dev/null +++ b/plugins/maps/forest_v2.py @@ -0,0 +1,182 @@ +# ba_meta require api 9 + +from __future__ import annotations +from typing import TYPE_CHECKING + +import bascenev1 as bs +from bascenev1 import _map +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + pass + +plugman = dict( + plugin_name="forest_v2", + description="A better looking land with some trees\nNew mini games added so you can play more on this update forest", + external_url="", + authors=[ + {"name": "Startingbat", "email": "", "discord": "startingbat"}, + ], + version="1.0.0", +) + + +class ForestMapData: + points = {} + boxes = {} + + boxes['area_of_interest_bounds'] = ( + (0.0, 1.185751251, 0.4326226188) + (0.0, 0.0, 0.0) + (29.8180273, 11.57249038, 18.89134176) + ) + boxes['edge_box'] = ( + (-0.103873591, 0.4133341891, 0.4294651013) + + (0.0, 0.0, 0.0) + + (22.48295719, 1.290242794, 8.990252454) + ) + boxes['map_bounds'] = ( + (0.0, 1.185751251, 0.4326226188) + (0.0, 0.0, 0.0) + (42.09506485, 22.81173179, 29.76723155) + ) + points['ffa_spawn1'] = (-2.0, -2.0, -4.373674593) + ( + 8.895057015, + 1.0, + 0.444350722, + ) + points['ffa_spawn2'] = (-2.0, -2.0, 2.076288941) + ( + 8.895057015, + 1.0, + 0.444350722, + ) + + points['flag_default'] = (-2.5, -3.0, -2.0) + points['powerup_spawn1'] = (-6.0, -2.6, -1.25) + points['powerup_spawn2'] = (1.0, -2.6, -1.25) + points['spawn1'] = (-10.0, -2.0, -2.0) + (0.5, 1.0, 3.2) + points['spawn2'] = (5.0, -2.0, -2.0) + (0.5, 1.0, 3.2) + points['race_point1'] = (0.5901776337, -2.6, 1.543598704) + ( + 0.2824957007, + 3.950514538, + 2.292534365, + ) + points['race_point2'] = (4.7526567, -2.6, 1.09551316) + ( + 0.2824957007, + 3.950514538, + 2.392880724, + ) + points['race_point3'] = (7.450800117, -2.6, -2.248040576) + ( + 2.167067932, + 3.950514538, + 0.2574992262, + ) + points['race_point4'] = (5.064768438, -2.6, -5.820463576) + ( + 0.2824957007, + 3.950514538, + 2.392880724, + ) + points['race_point5'] = (0.5901776337, -2.6, -6.165424036) + ( + 0.2824957007, + 3.950514538, + 2.156382533, + ) + points['race_point6'] = (-3.057459058, -2.6, -6.114179652) + ( + 0.2824957007, + 3.950514538, + 2.323773344, + ) + points['race_point7'] = (-5.814316926, -2.6, -2.248040576) + ( + 2.0364457, + 3.950514538, + 0.2574992262, + ) + points['race_point8'] = (-2.958397223, -2.6, 1.360005754) + ( + 0.2824957007, + 3.950514538, + 2.529692681, + ) + points['flag1'] = (-10.25842, -2.6673191, -2.2210996) + points['flag2'] = (5.2464933, -3.2587945, -1.6802032) + points['tnt1'] = (-0.08421587483, 0.9515026107, -0.7762602271) + + +class ForestMap(bs.Map): + + defs = ForestMapData() + name = 'Forest' + + @classmethod + def get_play_types(cls) -> list[str]: + return ['melee', 'keep_away', 'team_flag', 'race'] + + @classmethod + def get_preview_texture_name(cls) -> list[str]: + return 'natureBackgroundColor' + + @classmethod + def on_preload(cls) -> any: + data: dict[str, any] = { + 'mesh': bs.getmesh('natureBackground'), + 'tex': bs.gettexture('natureBackgroundColor'), + 'mesh2': bs.getmesh('trees'), + 'tex2': bs.gettexture('treesColor'), + 'collision_mesh': bs.getcollisionmesh('natureBackgroundCollide'), + 'mesh_bg': bs.getmesh('thePadBG'), + 'mesh_bg_tex': bs.gettexture('black'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + self.node = bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': self.preloaddata['mesh'], + 'color_texture': self.preloaddata['tex'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'materials': [shared.footing_material], + }, + ) + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['mesh_bg'], + 'lighting': False, + 'color_texture': self.preloaddata['mesh_bg_tex'], + }, + ) + self.trees = bs.newnode( + 'prop', + attrs={ + 'mesh': self.preloaddata['mesh2'], + 'body': 'box', + 'mesh_scale': 0.6, + 'density': 999999, # Very high density to make it immovable + 'damping': 999999, + 'position': (-2, 9, -5), + 'color_texture': self.preloaddata['tex2'], + }, + ) + + gnode = bs.getactivity().globalsnode + gnode.tint = (1.0, 1.10, 1.15) + gnode.ambient_color = (0.9, 1.3, 1.1) + gnode.shadow_ortho = True + gnode.shadow_offset = (0, 0, -5.0) + gnode.vignette_outer = (0.76, 0.76, 0.76) + gnode.vignette_inner = (0.95, 0.95, 0.99) + + def is_point_near_edge(self, point: bs.Vec3, running: bool = False) -> bool: + xpos = point.x + zpos = point.z + x_adj = xpos * 0.125 + z_adj = (zpos + 3.7) * 0.2 + if running: + x_adj *= 1.4 + z_adj *= 1.4 + return x_adj * x_adj + z_adj * z_adj > 1.0 + + +# ba_meta export babase.Plugin +class StartingbatYT(bs.Plugin): + _map.register_map(ForestMap) diff --git a/plugins/minigames.json b/plugins/minigames.json index e9aa2c091..969670f2a 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1,8 +1,288 @@ { "name": "Minigames", "description": "Minigames", - "plugins_base_url": "http://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/minigames", + "plugins_base_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/minigames", "plugins": { + "soccer": { + "description": "Shoot the ball in left or right edge of the map to score", + "external_url": "", + "authors": [ + { + "name": "Stary_Agent", + "email": "", + "discord": "" + } + ], + "versions": { + "2.0.0": { + "api_version": 8, + "commit_sha": "a941899", + "released_on": "20-07-2023", + "md5sum": "19f033445a8fe30fc7f4f62d94a54444" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "e59073b", + "released_on": "02-11-2022", + "md5sum": "ceb7ac417c85396722fa9f7fee3cfc01" + } + } + }, + "icy_emits": { + "description": "Survice from icy bombs emitting through cold platform. Playable in teams/ffa/co-op", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "" + } + ], + "versions": { + "2.0.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "58e760845f3d12bbac359a4bfab828da" + }, + "2.0.0": { + "api_version": 9, + "commit_sha": "6bf081a", + "released_on": "08-02-2025", + "md5sum": "ae390fd99e657a19a553c29a48c4cff4" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "48f9302", + "released_on": "28-07-2023", + "md5sum": "cedc448a3d625065d586445e115fcd1f" + } + } + }, + "frozen_one": { + "description": "Survive until the timer runs out", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "" + } + ], + "versions": { + "2.0.0": { + "api_version": 9, + "commit_sha": "6bf081a", + "released_on": "08-02-2025", + "md5sum": "10c482f1843894f4376b6624df276cb7" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "48f9302", + "released_on": "28-07-2023", + "md5sum": "529c8b9c9a450317d5aa75e8eab47497" + } + } + }, + "simon_says": { + "description": "You better do what Simon says. Playable in co-op", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.2": { + "api_version": 8, + "commit_sha": "593ae6f", + "released_on": "16-02-2024", + "md5sum": "57cd7a06f1f3169752cf77537affcf5b" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "3b55e82dc1c1d4d84760c23098233e30" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "def39482b339095a7affb574af829ea0" + } + } + }, + "squid_race": { + "description": "Race inspired by the squid games", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "74d61a487379d163c3f5713f001ec69d" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "7ec43481e6b35b4da4f6425977ee0004" + } + } + }, + "zombie_horde": { + "description": "Fight or defend against horde of zombies", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "25f9018fdc70173212e436d4e7c41e97" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "671e7b98a9d3eb0aeb7e47448db25af9" + } + } + }, + "meteor_shower": { + "description": "MeteorShower with many different bombs", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "4630220820b08642e9c72f9f24675298" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "1ce8c1a3f3ec240af39b7a73375e7891" + } + } + }, + "boxing": { + "description": "Simple boxing minigame", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "3.0.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "1c7889d24758596239b5a7b61e4843f1" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "6263872", + "released_on": "23-07-2023", + "md5sum": "9789ad3583f1d92d4e4b7bc03d09591d" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "c1b615df09886ba05622c4e8b4f6be40" + } + } + }, + "basket_bomb": { + "description": "Score all the baskets and be the MVP.", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.2.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "0e1287021cb279416366fe092dc4b60b" + }, + "1.1.0": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "6c90c97151c31d240a760590c56d7dbf" + }, + "1.0.1": { + "api_version": 7, + "commit_sha": "d511c15", + "released_on": "03-08-2023", + "md5sum": "a352bee7d9be68773bb056d94486a299" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "7ec87edbdd5183ec4b2daffad0019820" + } + } + }, + "ultimate_last_stand": { + "description": "Bring Last Stand minigame into team fight and FFA.", + "external_url": "", + "authors": [ + { + "name": "Cross Joy", + "email": "cross.joy.official@gmail.com", + "discord": "crossjoy" + } + ], + "versions": { + "2.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "c7d3cc971090ee5a4cb0f096dad9f118" + }, + "2.0.0": { + "api_version": 7, + "commit_sha": "8b257b3", + "released_on": "22-10-2022", + "md5sum": "4f99e4594cac5a9df137faa257f919dc" + } + } + }, "alliance_elimination": { "description": "Fight in groups of duo, trio, or more. Last remaining alive wins.", "external_url": "", @@ -10,18 +290,1379 @@ { "name": "Rikko", "email": "rikkolovescats@proton.me", - "discord": "Rikko#7383" + "discord": "rikkolovescats" + } + ], + "versions": { + "2.1.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "3a031a43656962ade9a818d8d44daba8" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "1b14789", + "released_on": "02-07-2023", + "md5sum": "cb2a7700dd13febe6f68c3cd979b8b19" + }, + "1.1.0": { + "api_version": 7, + "commit_sha": "40b70fe", + "released_on": "08-08-2022", + "md5sum": "f4f0bb91f5d10cf8f591ecf7d2848182" + } + } + }, + "volleyball": { + "description": "Play Volleyball in teams of two", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "" + } + ], + "versions": { + "3.0.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "9f204fa332ab46ab0e53dd790062d26e" + }, + "3.0.0": { + "api_version": 9, + "commit_sha": "07f7a85", + "released_on": "09-02-2025", + "md5sum": "ccfff509782e0f5d838fcc9813b4a3c7" + }, + "2.0.1": { + "api_version": 8, + "commit_sha": "d511c15", + "released_on": "03-08-2023", + "md5sum": "9277227c0f267f0d7d9e9d744f14688b" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "48f9302", + "released_on": "28-07-2023", + "md5sum": "2c457b80b5a35adf0cad1436af4ab3fe" + }, + "1.1.0": { + "api_version": 7, + "commit_sha": "0bc9522", + "released_on": "31-08-2022", + "md5sum": "a7ad7f1ac908fd871bcbcf665d49828c" + } + } + }, + "memory_game": { + "description": "Memorise tiles to surive! Playable in teams/ffa/co-op", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "" + } + ], + "versions": { + "3.0.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "2ee01edd812679e9cd9515a0a7c6a62f" + }, + "3.0.0": { + "api_version": 9, + "commit_sha": "6bf081a", + "released_on": "08-02-2025", + "md5sum": "67c9612e3bfdb0383737bcc1b32ef64b" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "48f9302", + "released_on": "28-07-2023", + "md5sum": "49ae645a5afc390ead44d7219b388c78" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "858030b", + "released_on": "01-09-2022", + "md5sum": "27de4d6a66f41367977812c2df307c24" + } + } + }, + "musical_flags": { + "description": "Musical chairs... but for bombsquad!?!! Playable in ffa/teams", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "" + } + ], + "versions": { + "3.0.0": { + "api_version": 9, + "commit_sha": "6bf081a", + "released_on": "08-02-2025", + "md5sum": "d606c10308c2a42a604cc74dcf92c8d1" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "48f9302", + "released_on": "28-07-2023", + "md5sum": "4cb6510f9f3ce151720a53a957986864" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "858030b", + "released_on": "01-09-2022", + "md5sum": "c84b7f415de5d3e9189ee73fc0e3ce93" + } + } + }, + "hot_potato": { + "description": "Pass the mark to someone else before you explode!", + "external_url": "", + "authors": [ + { + "name": "TheMikirog", + "email": "", + "discord": "themikirog" + } + ], + "versions": { + "3.0.0": { + "api_version": 9, + "commit_sha": "bdcb21a", + "released_on": "13-12-2025", + "md5sum": "1bc35805009df2f38d6443f9f4088d40" + }, + "1.0.2": { + "api_version": 9, + "commit_sha": "0225234", + "released_on": "22-02-2025", + "md5sum": "2273f55338718a4d4c3db55ec9b01516" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "3272ee5dd06d5ad603c6a1be189ad1d1" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "0841b9e", + "released_on": "14-11-2022", + "md5sum": "ec3980f3f3a5da96c27f4cbd61f98550" + } + } + }, + "hot_bomb": { + "description": "Get the bomb to explode on the enemy team to win.", + "external_url": "", + "authors": [ + { + "name": "SEBASTIAN2059", + "email": "", + "discord": "sebastian2059" + }, + { + "name": "zPanxo", + "email": "", + "discord": "zPanxo#7201" + } + ], + "versions": { + "1.1.0": { + "api_version": 8, + "commit_sha": "0069f15", + "released_on": "08-06-2023", + "md5sum": "b8168bd3e6fae96b9339a727bd50842f" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "095a773", + "released_on": "29-04-2023", + "md5sum": "7296a71c9ed796ab1f54c2eb5d843bda" + } + } + }, + "shimla": { + "description": "Death match with elevators, 2-D view.", + "external_url": "https://www.youtube.com/channel/UCaQajfKHrTPgiOhuias5iPg", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "smoothy@bombsquad.ga", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "3730cdc9f922ac5a8c86e2c7debd09fe" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "52094fc", + "released_on": "06-05-2023", + "md5sum": "d4303c1338df50aa15c718cfd9c4ba0a" + } + } + }, + "demolition_war": { + "description": "BombFight on wooden floor flying in air.", + "external_url": "https://www.youtube.com/channel/UCaQajfKHrTPgiOhuias5iPg", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "smoothy@bombsquad.ga", + "discord": "mr.smoothy" + } + ], + "versions": { + "3.0.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "1c91602f51ad2b0c34bfc1be484b7465" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "f54c993", + "released_on": "27-07-2023", + "md5sum": "7699483f4c379db809676ac917943d3c" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2aa1e50", + "released_on": "04-06-2023", + "md5sum": "096107aaa9dad6d2753aa462c0a48537" + } + } + }, + "castel_queen": { + "description": "Carry the queen for some duration in her room.", + "external_url": "https://www.youtube.com/channel/UCaQajfKHrTPgiOhuias5iPg", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "smoothy@bombsquad.ga", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "2ebff56d18bd106270918a99352d78c5" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "a1d2d75303aaeb0b078a4c836d65ebee" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2aa1e50", + "released_on": "04-06-2023", + "md5sum": "74e876139e20a392c15deeafa31354f7" + } + } + }, + "drone_war": { + "description": "Fly with Drone and attack with rocket launcher", + "external_url": "https://www.youtube.com/channel/UCaQajfKHrTPgiOhuias5iPg", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "smoothy@bombsquad.ga", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.0.2": { + "api_version": 8, + "commit_sha": "78bc781", + "released_on": "22-08-2024", + "md5sum": "506ad0369c82b2fb957da66fe9906988" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "aa03952868be8e978d7b23891883b52a" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2aa1e50", + "released_on": "04-06-2023", + "md5sum": "569219eb72d8c27271f20fe375a01a69" + } + } + }, + "the_spaz_game": { + "description": "Enemy Spaz Amoung us, kill correct person.", + "external_url": "https://www.youtube.com/channel/UCaQajfKHrTPgiOhuias5iPg", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "smoothy@bombsquad.ga", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "3a99f089fda47e034b16e7fa086ee051" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2aa1e50", + "released_on": "04-06-2023", + "md5sum": "55021c091b640780d6280cfb4b56b664" + } + } + }, + "air_soccer": { + "description": "Play soccer while flying in air", + "external_url": "https://youtu.be/j6FFk7E6W_U", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "smoothy@bombsquad.ga", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "7e131c4", + "released_on": "26-01-2025", + "md5sum": "271cec6b67d72bf1a7ee6c5f4205df0c" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "627390f50e18bd76d94abf35f538ec8f" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "52094fc", + "released_on": "06-05-2023", + "md5sum": "4eb4a07830e4bd52fd19150a593d70c5" + } + } + }, + "canon_fight": { + "description": "Blow up your enemy with powerfull cannon", + "external_url": "https://www.youtube.com/watch?v=7cv3ZSZeTns", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "db9b710adac00b4d16a9f93d46c56e52" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "505c948", + "released_on": "09-06-2024", + "md5sum": "44adbe3bd6ec4988d65ff4ee79b48d95" + } + } + }, + "laser_tracer": { + "description": "Dont touch dangerous laser light", + "external_url": "https://youtu.be/wTgwZKiykQw?si=Cr0ybDYAcKCUNFN4", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "505c948", + "released_on": "09-06-2024", + "md5sum": "08a6457ebd271a7f7860a506b731d272" + } + } + }, + "arms_race": { + "description": "Upgrade your weapons by eliminating enemies. Win by being first one to kill while cursed", + "external_url": "", + "authors": [ + { + "name": "Mrmaxmeier", + "email": "", + "discord": "" + }, + { + "name": "Freaku", + "email": "", + "discord": "" + } + ], + "versions": { + "2.1.1": { + "api_version": 9, + "commit_sha": "6bf081a", + "released_on": "08-02-2025", + "md5sum": "4feb8d6a90fee305ea70531fc9bb0e31" + }, + "2.1.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "fc26523e8904dbfca93cd9271d429640" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "48f9302", + "released_on": "28-07-2023", + "md5sum": "22b51a147524d84fbc249e61f21ae424" + }, + "1.1.0": { + "api_version": 7, + "commit_sha": "2e2540a", + "released_on": "20-05-2023", + "md5sum": "e06fbfeb1c0c4c78e04054caab3d593f" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "7219487", + "released_on": "15-05-2023", + "md5sum": "1482bb521329ec10ab109d9414c0cc6b" + } + } + }, + "collector": { + "description": "Kill your opponents to steal their Capsules. Collect them and score at the Deposit Point!", + "external_url": "", + "authors": [ + { + "name": "TheMikirog", + "email": "", + "discord": "themikirog" + }, + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "3.0.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "7f5f51ee190ab6a1f5f569a1bc8abc49" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "e8bbb61", + "released_on": "23-07-2023", + "md5sum": "1acbeecffada937bdd745f4e4d43f1be" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "7219487", + "released_on": "15-05-2023", + "md5sum": "afca5167b564a7f7c0bdc53c3e52d198" + } + } + }, + "dodge_the_ball": { + "description": "Survive from shooting balls", + "external_url": "", + "authors": [ + { + "name": "EmperoR", + "email": "", + "discord": "EmperoR#4098" + } + ], + "versions": { + "2.0.0": { + "api_version": 8, + "commit_sha": "1a8037e", + "released_on": "23-07-2023", + "md5sum": "916e37f6e1a8a5be3dd0389ed2c4b261" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "7219487", + "released_on": "15-05-2023", + "md5sum": "6951991607ecef5a6a93cc6eb9e560f5" + } + } + }, + "invisible_one": { + "description": "Be the invisible one for a length of time to win. Kill the invisible one to become it.", + "external_url": "", + "authors": [ + { + "name": "itsre3", + "email": "", + "discord": "itsre3#0267" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "7e741a7f4c1ace1124d8719a009f8948" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "7219487", + "released_on": "15-05-2023", + "md5sum": "bc2f85bb6149abbbd496c1b07f3a7189" + } + } + }, + "last_punch_stand": { + "description": "Last one to punch the spaz when timer ends wins", + "external_url": "", + "authors": [ + { + "name": "ThePersonMan", + "email": "", + "discord": "the.personman" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "270961a492432e6199dec2d0915d8acf" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "7219487", + "released_on": "15-05-2023", + "md5sum": "410c3a275970d0c358125dd2edf035e0" + } + } + }, + "quake": { + "description": "Kill set number of enemies to win with optional rocket/railgun weapons", + "external_url": "", + "authors": [ + { + "name": "Dliwk", + "email": "", + "discord": "dliwk" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "3fff070", + "released_on": "22-09-2025", + "md5sum": "a245b38b3783d3d5a865c94c89974e6f" + }, + "1.0.2": { + "api_version": 8, + "commit_sha": "da878bf", + "released_on": "25-04-2024", + "md5sum": "bac58caf1703d8f2f8ac86821c489eef" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "3fb424583f1e686854fe94bd22c5161c" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "7219487", + "released_on": "15-05-2023", + "md5sum": "a771fb7d37dad80ed29a99bd5ca458d6" + } + } + }, + "sleep_race": { + "description": "Can you win while sleeping?", + "external_url": "", + "authors": [ + { + "name": "itsre3", + "email": "", + "discord": "itsre3#0267" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "7e70fb037b49b183a9fb4eaa2babb90e" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "7219487", + "released_on": "15-05-2023", + "md5sum": "c8ea242e4b0b0110541370a2ce1b42fc" + } + } + }, + "snake": { + "description": "Survive a set number of mines to win", + "external_url": "", + "authors": [ + { + "name": "SEBASTIAN2059", + "email": "", + "discord": "sebastian2059" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "febeeb370ac150f455ed27bc9d557d75" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "7219487", + "released_on": "15-05-2023", + "md5sum": "cfd9a320ee6fd7b8adb3cae1015c67fa" + } + } + }, + "quake_original": { + "description": "Good ol' Quake minigame", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "185480d", + "released_on": "24-07-2023", + "md5sum": "f68395cc90dc8cddb166a23b2da81b7b" + } + } + }, + "ufo_fight": { + "description": "Fight the UFO boss!", + "external_url": "", + "authors": [ + { + "name": "Cross Joy", + "email": "cross.joy.official@gmail.com", + "discord": "crossjoy" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "82ffbb28961c57731bd64d4c4add06cd" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "7219487", + "released_on": "15-05-2023", + "md5sum": "81617b130716996368b7d8f20f3a5154" + } + } + }, + "yeeting_party": { + "description": "Yeet your enemies out of the map!", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "" + } + ], + "versions": { + "2.0.0": { + "api_version": 8, + "commit_sha": "8f281f9", + "released_on": "30-07-2023", + "md5sum": "c512cd21244f9537b7ff98438a3e6ef8" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "7219487", + "released_on": "15-05-2023", + "md5sum": "197a377652ab0c3bfbe1ca07833924b4" + } + } + }, + "big_ball": { + "description": "Score some goals with Big Ball on Football map", + "external_url": "", + "authors": [ + { + "name": "MythB", + "email": "", + "discord": "" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "add648ab6cda8f3345f856e032431249" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "ca3221b", + "released_on": "27-07-2023", + "md5sum": "39c5b18efd5d5314f30c12fc0bec4931" + } + } + }, + "botsvsbots": { + "description": "Watch your team of bots fight the opposing team of bots!", + "external_url": "", + "authors": [ + { + "name": "rabbitboom", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.1": { + "api_version": 9, + "commit_sha": "7cf3de7", + "released_on": "27-11-2025", + "md5sum": "d0bd3ee5a83d877dfcca74a7768024e0" + }, + "1.0.0": { + "api_version": 9, + "commit_sha": "25b58cb", + "released_on": "28-10-2025", + "md5sum": "bbe28558ce336f5f5d5d5a21305c5fd4" + } + } + }, + "handball": { + "description": "Score some goals with handball", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "2309aab", + "released_on": "24-07-2023", + "md5sum": "01b85dc9ef1d464ab604387af09f05dc" + } + } + }, + "bomb_on_my_head": { + "description": "Bomb on your head, Survive as long as you can!", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "287ec8c8c7d7fa62b0856044ab44d853" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "c39664c", + "released_on": "22-08-2023", + "md5sum": "daf75409557f12146c1a63d61f429e10" + } + } + }, + "infection": { + "description": "It's spreading, avoid the spread!", + "external_url": "", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "5063690", + "released_on": "03-02-2024", + "md5sum": "4a9cdcd798454e5034a5ea9ce58fe586" + } + } + }, + "super_duel": { + "description": "Crush your enemy in a 1v1", + "external_url": "https://www.youtube.com/watch?v=hvRtiXR-oC0", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "ab26786", + "released_on": "27-07-2023", + "md5sum": "d0c9c0472d7b145eadf535957a462fdf" + } + } + }, + "supersmash": { + "description": "Blow up your enemies off the map!", + "external_url": "", + "authors": [ + { + "name": "Mrmaxmeier", + "email": "", + "discord": "" + }, + { + "name": "JoseAngel", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.1.0": { + "api_version": 8, + "commit_sha": "6a66826", + "released_on": "12-10-2023", + "md5sum": "f71b146868554b61b7b1fe3d393a5859" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "68e77f2", + "released_on": "24-07-2023", + "md5sum": "1cbe5b3e85b5dfcee1eb322f33568fd4" + } + } + }, + "avalanche": { + "description": "Dodge the falling ice bombs", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "aeaa59b02df275bafa2de28283bfd60d" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "5063690", + "released_on": "03-02-2024", + "md5sum": "a07a171c29417056bb69ed1cf0f2864b" + } + } + }, + "hyper_race": { + "description": "Race and avoid the obstacles", + "external_url": "", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "5063690", + "released_on": "03-02-2024", + "md5sum": "8b423bdae256bd411489528b550b8bd9" + } + } + }, + "meteor_shower_deluxe": { + "description": "Meteor shower on all maps support", + "external_url": "", + "authors": [ + { + "name": "EraOSBeta", + "email": "", + "discord": "3ra0" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "718039b", + "released_on": "24-01-2024", + "md5sum": "7e86bbc8e3ebb26a66602068950adfbf" + } + } + }, + "ofuuu_attack": { + "description": "Dodge the falling bombs.", + "external_url": "", + "authors": [ + { + "name": "Riyukii", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "718039b", + "released_on": "24-01-2024", + "md5sum": "982c72f2d2cb3d50280f50b022e7865f" + } + } + }, + "snow_ball_fight": { + "description": "Throw snoballs and dominate", + "external_url": "https://youtu.be/uXyb_meBjGI?si=D_N_OXZT5BFh8R5C", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "718039b", + "released_on": "24-01-2024", + "md5sum": "ef6bd7cd0404674f65e8f8d4da3ab8c8" + } + } + }, + "egg_game": { + "description": "Throw Egg as far u can", + "external_url": "https://youtu.be/82vLp9ceCcw?si=OSC5Hu3Ns7PevlwP", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "80bbbce", + "released_on": "27-06-2025", + "md5sum": "990e426d79f8836eee5aaab169e24fa2" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "388e58a", + "released_on": "01-02-2024", + "md5sum": "8fc1f9c984f9e77b0125cbeaba5c13bf" + } + } + }, + "you_vs_bombsquad": { + "description": "You against bombsquad solo or with friends. Playable in co-op", + "external_url": "", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "9ca6039", + "released_on": "15-02-2024", + "md5sum": "cb4698e70fd94402eca199250564ffba" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "718039b", + "released_on": "24-01-2024", + "md5sum": "451daeabbd628ec70fa31b80a0999f35" + } + } + }, + "onslaught_football": { + "description": "Onslaught but in football map", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "4941d0c", + "released_on": "01-02-2024", + "md5sum": "e6267fec4dc7f874b1f8065863aae1cb" + } + } + }, + "lame_fight": { + "description": "Save World With Super Powers. Playable in co-op", + "external_url": "", + "authors": [ + { + "name": "Blitz", + "email": "", + "discord": "itsmeblitz" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "9ca6039", + "released_on": "15-02-2024", + "md5sum": "9072f98eef0739474b2839a0866be969" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "4941d0c", + "released_on": "01-02-2024", + "md5sum": "eb0e5a8658e82b3aac6b04b68c1c0f60" + } + } + }, + "infinite_ninjas": { + "description": "How long can you survive from Ninjas??", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "5063690", + "released_on": "03-02-2024", + "md5sum": "15e303e02e3da4636fd002c43a579180" + } + } + }, + "gravity_falls": { + "description": "Trip to the moon", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "4941d0c", + "released_on": "01-02-2024", + "md5sum": "6d0b06b283ef702f41837f09e457d3b8" + } + } + }, + "bot_chase": { + "description": "Try to survive from bots! Playable in co-op", + "external_url": "", + "authors": [ + { + "name": "! JETZ", + "email": "", + "discord": "! JETZ#5313" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "3e6aaaff87e8d57b480a8bfde76dee05" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "eeeb17466a004db5117a477cb38aeb40" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "9ca6039", + "released_on": "15-02-2024", + "md5sum": "209ddaa4c4e128650730771db4d0588c" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "5063690", + "released_on": "03-02-2024", + "md5sum": "b0519f41146eb2f8b9c2d6c747376a9b" + } + } + }, + "down_into_the_abyss": { + "description": "Survive as long as you can but dont miss a step", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "4941d0c", + "released_on": "01-02-2024", + "md5sum": "d3c488671dd35c488e22002ddeb80aef" + } + } + }, + "better_deathmatch": { + "description": "A very-customisable DeathMatch mini-game", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "freakyyyy" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "0a8d5a054770a8c10e46bc14e2201f79" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "4941d0c", + "released_on": "01-02-2024", + "md5sum": "0b607db2dbe3ab40aa05bd4bfd5b4afa" + } + } + }, + "better_elimination": { + "description": "A very-customisable Elimination mini-game", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "freakyyyy" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "37c16ff71a7ef7ffc2c32e56d7cc27cf" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "4941d0c", + "released_on": "01-02-2024", + "md5sum": "18bbb7f6ddc4206c7039ad3b5f5facae" + } + } + }, + "bot_shower": { + "description": "Survive from the bots.", + "external_url": "", + "authors": [ + { + "name": "! JETZ", + "email": "", + "discord": "! JETZ#5313" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "17e968d", + "released_on": "26-01-2025", + "md5sum": "310db20ca310b7b8657e9dff0023c1af" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "4941d0c", + "released_on": "01-02-2024", + "md5sum": "1259e09ccd9b10ae82e4a6623572a4d2" + } + } + }, + "explodo_run": { + "description": "Cursed meteor shower of crazy Captain Jack trying take your soul. Playable in co-op", + "external_url": "", + "authors": [ + { + "name": "Blitz", + "email": "", + "discord": "itsmeblitz" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "9ca6039", + "released_on": "15-02-2024", + "md5sum": "4d711d5eb874cd63a81e9499abb74dd1" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "ec6286e", + "released_on": "01-02-2024", + "md5sum": "e3cd927316b9c3bcac714b4dc8d0d73c" + } + } + }, + "extinction": { + "description": "Survive the Extinction. Playable in co-op", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "9ca6039", + "released_on": "15-02-2024", + "md5sum": "1e74b3d2e38ac310a1b3fc28e13833dd" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "5063690", + "released_on": "03-02-2024", + "md5sum": "9fb61df79a50d010864964a7cb1de76e" + } + } + }, + "fat_pigs": { + "description": "Eliminate other Mels' while dodging falling stickies.", + "external_url": "", + "authors": [ + { + "name": "Zacker Tz", + "email": "", + "discord": "zacker_tz" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "ec6286e", + "released_on": "01-02-2024", + "md5sum": "fe0d82c322ce01d8956af3131e135ad2" + } + } + }, + "flag_day": { + "description": "Pick up flags to receive a prize.But beware...", + "external_url": "https://youtu.be/ANDzdBicjA4?si=h8S_TPUAxSaG7nls", + "authors": [ + { + "name": "MattZ45986", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "ec6286e", + "released_on": "01-02-2024", + "md5sum": "e11fbf742e6556939ac8d986e2dd430b" + } + } + }, + "gosquad_elimination": { + "description": "Elimination game with big bombs meteor shower (optional), as seen in the GoSquad server.", + "external_url": "https://discord.gg/QmzTtvyUvd", + "authors": [ + { + "name": "goblogger", + "email": "", + "discord": "goblogger" } ], "versions": { - "1.1.0": { - "api_version": 7, - "commit_sha": "cbdb3ead", - "dependencies": [], - "released_on": "08-08-2022", - "md5sum": "11dbb3c7e37e97bda028ea1251529ea0" + "1.0.1": { + "api_version": 9, + "commit_sha": "e11cfbb", + "released_on": "27-11-2025", + "md5sum": "25ae1788537855afe7ee832b18f6487f" + }, + "1.0.0": { + "api_version": 9, + "commit_sha": "a0e13b5", + "released_on": "26-11-2025", + "md5sum": "33e6b93420cf6115e992aca534d76bfd" } } } } -} +} \ No newline at end of file diff --git a/plugins/minigames/air_soccer.py b/plugins/minigames/air_soccer.py new file mode 100644 index 000000000..746909c89 --- /dev/null +++ b/plugins/minigames/air_soccer.py @@ -0,0 +1,659 @@ +# Released under the MIT License. See LICENSE for details. +# BY Stary_Agent +"""Hockey game and support classes.""" + +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + + +def create_slope(self): + shared = SharedObjects.get() + x = 5 + y = 12 + for i in range(0, 10): + bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': (0.2, 0.1, 6), + 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + x = x+0.3 + y = y+0.1 + + +class Puck(bs.Actor): + """A lovely giant hockey puck.""" + + def __init__(self, position: Sequence[float] = (0.0, 13.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + assert activity is not None + assert isinstance(activity, HockeyGame) + pmats = [shared.object_material, activity.puck_material] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': activity.puck_mesh, + 'color_texture': activity.puck_tex, + 'body': 'sphere', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'gravity_scale': 0.3, + 'shadow_size': 0.5, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class AirSoccerGame(bs.TeamGameActivity[Player, Team]): + """Ice hockey game.""" + + name = 'Epic Air Soccer' + description = 'Score some goals.' + available_settings = [ + bs.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.1), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Creative Thoughts'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self.slow_motion = True + self._scoreboard = Scoreboard() + self._cheer_sound = bs.getsound('cheer') + self._chant_sound = bs.getsound('crowdChant') + self._foghorn_sound = bs.getsound('foghorn') + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self.puck_mesh = bs.getmesh('bomb') + self.puck_tex = bs.gettexture('landMine') + self.puck_scored_tex = bs.gettexture('landMineLit') + self._puck_sound = bs.getsound('metalHit') + self.puck_material = bs.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.puck_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._puck_sound, 0.2, 5)) + self._real_wall_material = bs.Material() + self._real_wall_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + + self._real_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._goal_post_material = bs.Material() + self._goal_post_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + + self._goal_post_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + + )) + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', bs.DieMessage()))) + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[bs.NodeActor]] = None + self._puck: Optional[Puck] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Score a goal.' + return 'Score ${ARG1} goals.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'score a goal' + return 'score ${ARG1} goals', self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._puck_spawn_pos = (0, 16.9, -5.5) + self._spawn_puck() + self.make_map() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (17, 14.5, -5.52), + 'scale': (1, 3, 1), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (-17, 14.5, -5.52), + 'scale': (1, 3, 1), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._update_scoreboard() + self._chant_sound.play() + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = bs.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + puck.last_players_to_touch[player.team.id] = player + + def make_map(self): + shared = SharedObjects.get() + bs.get_foreground_host_activity()._map.leftwall.materials = [ + shared.footing_material, self._real_wall_material] + + bs.get_foreground_host_activity()._map.rightwall.materials = [ + shared.footing_material, self._real_wall_material] + + bs.get_foreground_host_activity()._map.topwall.materials = [ + shared.footing_material, self._real_wall_material] + self.floorwall = bs.newnode('region', attrs={'position': (0, 5, -5.52), 'scale': ( + 35.4, 0.2, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': ( + 0, 5, -5.52), 'color': (0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (35.4, 0.2, 2)}) + + self.create_goal_post(-16.65, 12.69) + self.create_goal_post(-16.65, 16.69) + + self.create_goal_post(16.65, 12.69) + self.create_goal_post(16.65, 16.69) + + self.create_static_step(0, 16.29) + + self.create_static_step(4.35, 11.1) + self.create_static_step(-4.35, 11.1) + + self.create_vertical(10, 15.6) + self.create_vertical(-10, 15.6) + + def create_static_step(self, x, y): + floor = "" + for i in range(0, 7): + floor += "_ " + shared = SharedObjects.get() + step = {} + step["r"] = bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': ( + 3, 0.1, 6), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': ( + x, y, -5.52), 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (3, 0.1, 2)}) + + return step + + def create_goal_post(self, x, y): + shared = SharedObjects.get() + if x > 0: + color = (1, 0, 0) # change to team specific color + else: + color = (0, 0, 1) + floor = "" + for i in range(0, 4): + floor += "_ " + bs.newnode('region', attrs={'position': (x-0.2, y, -5.52), 'scale': (1.8, 0.1, 6), + 'type': 'box', 'materials': [shared.footing_material, self._goal_post_material]}) + + bs.newnode('locator', attrs={'shape': 'box', 'position': ( + x-0.2, y, -5.52), 'color': color, 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (1.8, 0.1, 2)}) + + def create_vertical(self, x, y): + shared = SharedObjects.get() + floor = "" + for i in range(0, 4): + floor += "|\n" + bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': (0.1, 2.8, 1), + 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': ( + x, y, -5.52), 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.1, 2.8, 2)}) + + def spawn_player_spaz(self, + player: Player, + position: Sequence[float] = None, + angle: float = None) -> PlayerSpaz: + """Intercept new spazzes and add our team material for them.""" + if player.team.id == 0: + position = (-10.75152479, 5.057427485, -5.52) + elif player.team.id == 1: + position = (8.75152479, 5.057427485, -5.52) + + spaz = super().spawn_player_spaz(player, position, angle) + return spaz + + def _kill_puck(self) -> None: + self._puck = None + + def _handle_score(self) -> None: + """A point has been scored.""" + + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = bs.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 20, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + self._foghorn_sound.play() + self._cheer_sound.play() + + self._puck.scored = True + + # Change puck texture to something cool + self._puck.node.color_texture = self.puck_scored_tex + # Kill the puck (it'll respawn itself shortly). + bs.timer(1.0, self._kill_puck) + + light = bs.newnode('light', + attrs={ + 'position': bs.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + bs.timer(1.0, light.delete) + + bs.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + bs.timer(3.0, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + light = bs.newnode('light', + attrs={ + 'position': self._puck_spawn_pos, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) + + def _spawn_puck(self) -> None: + self._swipsound.play() + self._whistle_sound.play() + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) + + +class mapdefs: + points = {} + # noinspection PyDictCreation + boxes = {} + boxes['area_of_interest_bounds'] = (-1.045859963, 12.67722855, + -5.401537075) + (0.0, 0.0, 0.0) + ( + 42.46156851, 20.94044653, 0.6931564611) + points['ffa_spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) + points['ffa_spawn2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) + points['ffa_spawn3'] = (9.55724115, 11.30789446, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn4'] = (-11.55747023, 10.99170684, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn5'] = (-1.878892369, 9.46490571, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn6'] = (-0.4912812943, 5.077006397, -5.521672101) + ( + 1.878332089, 1.453808816, 0.007578097856) + points['flag1'] = (-11.75152479, 8.057427485, -5.52) + points['flag2'] = (9.840909039, 8.188634282, -5.52) + points['flag3'] = (-0.2195258696, 5.010273907, -5.52) + points['flag4'] = (-0.04605809154, 12.73369108, -5.52) + points['flag_default'] = (-0.04201942896, 12.72374492, -5.52) + boxes['map_bounds'] = (-0.8748348681, 9.212941713, -5.729538885) + ( + 0.0, 0.0, 0.0) + (42.09666006, 26.19950145, 7.89541168) + points['powerup_spawn1'] = (1.160232442, 6.745963662, -5.469115985) + points['powerup_spawn2'] = (-1.899700206, 10.56447241, -5.505721177) + points['powerup_spawn3'] = (10.56098871, 12.25165669, -5.576232453) + points['powerup_spawn4'] = (-12.33530337, 12.25165669, -5.576232453) + points['spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) + points['spawn2'] = (7.484707127, 8.172681752, + -5.614479365) + (1.553861796, 1.453808816, 0.04419853907) + points['spawn_by_flag1'] = (-9.295167711, 8.010664315, -5.44451005) + ( + 1.555840357, 1.453808816, 0.1165648888) + points['spawn_by_flag2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) + points['spawn_by_flag3'] = (-1.45994593, 5.038762459, -5.535288724) + ( + 0.9516389866, 0.6666414677, 0.08607244075) + points['spawn_by_flag4'] = (0.4932087091, 12.74493212, -5.598987003) + ( + 0.5245740665, 0.5245740665, 0.01941146064) + + +class CreativeThoughts(bs.Map): + """Freaking map by smoothy.""" + + defs = mapdefs + + name = 'Creative Thoughts' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [ + 'melee', 'keep_away', 'team_flag' + ] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'alwaysLandPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'mesh': bs.getmesh('alwaysLandLevel'), + 'bottom_mesh': bs.getmesh('alwaysLandLevelBottom'), + 'bgmesh': bs.getmesh('alwaysLandBG'), + 'collision_mesh': bs.getcollisionmesh('alwaysLandLevelCollide'), + 'tex': bs.gettexture('alwaysLandLevelColor'), + 'bgtex': bs.gettexture('alwaysLandBGColor'), + 'vr_fill_mound_mesh': bs.getmesh('alwaysLandVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound') + } + return data + + @classmethod + def get_music_type(cls) -> bs.MusicType: + return bs.MusicType.FLYING + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, -3.7, 2.5)) + shared = SharedObjects.get() + self._fake_wall_material = bs.Material() + self._real_wall_material = bs.Material() + self._fake_wall_material.add_actions( + conditions=(('they_are_younger_than', 9000), 'and', + ('they_have_material', shared.player_material)), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._real_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + 'background': True, + 'color_texture': bs.gettexture("rampageBGColor") + }) + + self.leftwall = bs.newnode('region', attrs={'position': (-17.75152479, 13, -5.52), 'scale': ( + 0.1, 15.5, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.rightwall = bs.newnode('region', attrs={'position': (17.75, 13, -5.52), 'scale': ( + 0.1, 15.5, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.topwall = bs.newnode('region', attrs={'position': (0, 21.0, -5.52), 'scale': ( + 35.4, 0.2, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (-17.75152479, 13, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.1, 15.5, 2)}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (17.75, 13, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.1, 15.5, 2)}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (0, 21.0, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (35.4, 0.2, 2)}) + + gnode = bs.getactivity().globalsnode + gnode.happy_thoughts_mode = True + gnode.shadow_offset = (0.0, 8.0, 5.0) + gnode.tint = (1.3, 1.23, 1.0) + gnode.ambient_color = (1.3, 1.23, 1.0) + gnode.vignette_outer = (0.64, 0.59, 0.69) + gnode.vignette_inner = (0.95, 0.95, 0.93) + gnode.vr_near_clip = 1.0 + self.is_flying = True + + # throw out some tips on flying + txt = bs.newnode('text', + attrs={ + 'text': babase.Lstr(resource='pressJumpToFlyText'), + 'scale': 1.2, + 'maxwidth': 800, + 'position': (0, 200), + 'shadow': 0.5, + 'flatness': 0.5, + 'h_align': 'center', + 'v_attach': 'bottom' + }) + cmb = bs.newnode('combine', + owner=txt, + attrs={ + 'size': 4, + 'input0': 0.3, + 'input1': 0.9, + 'input2': 0.0 + }) + bs.animate(cmb, 'input3', {3.0: 0, 4.0: 1, 9.0: 1, 10.0: 0}) + cmb.connectattr('output', txt, 'color') + bs.timer(10.0, txt.delete) + + +try: + bs._map.register_map(CreativeThoughts) +except: + pass diff --git a/plugins/minigames/alliance_elimination.py b/plugins/minigames/alliance_elimination.py index e6f377617..b8255a6d5 100644 --- a/plugins/minigames/alliance_elimination.py +++ b/plugins/minigames/alliance_elimination.py @@ -2,23 +2,25 @@ # """Elimination mini-game.""" -# ba_meta require api 7 +# ba_meta require api 9 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations from typing import TYPE_CHECKING -import ba -from bastd.actor.spazfactory import SpazFactory -from bastd.actor.scoreboard import Scoreboard +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.scoreboard import Scoreboard if TYPE_CHECKING: from typing import (Any, Tuple, Type, List, Sequence, Optional, Union) -class Icon(ba.Actor): +class Icon(bs.Actor): """Creates in in-game icon on screen.""" def __init__(self, @@ -37,10 +39,10 @@ def __init__(self, self._show_lives = show_lives self._show_death = show_death self._name_scale = name_scale - self._outline_tex = ba.gettexture('characterIconMask') + self._outline_tex = bs.gettexture('characterIconMask') icon = player.get_icon() - self.node = ba.newnode('image', + self.node = bs.newnode('image', delegate=self, attrs={ 'texture': icon['texture'], @@ -53,12 +55,12 @@ def __init__(self, 'absolute_scale': True, 'attach': 'bottomCenter' }) - self._name_text = ba.newnode( + self._name_text = bs.newnode( 'text', owner=self.node, attrs={ - 'text': ba.Lstr(value=player.getname()), - 'color': ba.safecolor(player.team.color), + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), 'h_align': 'center', 'v_align': 'center', 'vr_depth': 410, @@ -69,7 +71,7 @@ def __init__(self, 'v_attach': 'bottom' }) if self._show_lives: - self._lives_text = ba.newnode('text', + self._lives_text = bs.newnode('text', owner=self.node, attrs={ 'text': 'x0', @@ -125,7 +127,7 @@ def handle_player_died(self) -> None: if not self.node: return if self._show_death: - ba.animate( + bs.animate( self.node, 'opacity', { 0.00: 1.0, 0.05: 0.0, @@ -142,16 +144,16 @@ def handle_player_died(self) -> None: }) lives = self._player.lives if lives == 0: - ba.timer(0.6, self.update_for_lives) + bs.timer(0.6, self.update_for_lives) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): self.node.delete() return None return super().handlemessage(msg) -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: @@ -159,7 +161,7 @@ def __init__(self) -> None: self.icons: List[Icon] = [] -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: @@ -167,14 +169,14 @@ def __init__(self) -> None: self.spawn_order: List[Player] = [] -# ba_meta export game -class AllianceEliminationGame(ba.TeamGameActivity[Player, Team]): +# ba_meta export bascenev1.GameActivity +class AllianceEliminationGame(bs.TeamGameActivity[Player, Team]): """Game type where last player(s) left alive win.""" name = 'Alliance Elimination' description = 'Fight in groups of duo, trio, or more.\nLast remaining alive wins.' - scoreconfig = ba.ScoreConfig(label='Survived', - scoretype=ba.ScoreType.SECONDS, + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, none_is_winner=True) # Show messages when players die since it's meaningful here. announce_player_deaths = True @@ -183,23 +185,23 @@ class AllianceEliminationGame(ba.TeamGameActivity[Player, Team]): @classmethod def get_available_settings( - cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: settings = [ - ba.IntSetting( + bs.IntSetting( 'Lives Per Player', default=1, min_value=1, max_value=10, increment=1, ), - ba.IntSetting( + bs.IntSetting( 'Players Per Team In Arena', default=2, min_value=2, max_value=10, increment=1, ), - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), @@ -211,7 +213,7 @@ def get_available_settings( ], default=0, ), - ba.FloatChoiceSetting( + bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), @@ -222,27 +224,27 @@ def get_available_settings( ], default=1.0, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] - if issubclass(sessiontype, ba.DualTeamSession): + if issubclass(sessiontype, bs.DualTeamSession): settings.append( - ba.BoolSetting('Balance Total Lives', default=False)) + bs.BoolSetting('Balance Total Lives', default=False)) return settings @classmethod - def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: - return issubclass(sessiontype, ba.DualTeamSession) + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) @classmethod - def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: - return ba.getmaps('melee') + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('melee') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._start_time: Optional[float] = None - self._vs_text: Optional[ba.Actor] = None - self._round_end_timer: Optional[ba.Timer] = None + self._vs_text: Optional[bs.Actor] = None + self._round_end_timer: Optional[bs.Timer] = None self._epic_mode = bool(settings['Epic Mode']) self._lives_per_player = int(settings['Lives Per Player']) self._time_limit = float(settings['Time Limit']) @@ -253,16 +255,16 @@ def __init__(self, settings: dict): # Base class overrides: self.slow_motion = self._epic_mode - self.default_music = (ba.MusicType.EPIC - if self._epic_mode else ba.MusicType.SURVIVAL) + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) def get_instance_description(self) -> Union[str, Sequence]: return 'Last team standing wins.' if isinstance( - self.session, ba.DualTeamSession) else 'Last one standing wins.' + self.session, bs.DualTeamSession) else 'Last one standing wins.' def get_instance_description_short(self) -> Union[str, Sequence]: return 'last team standing wins' if isinstance( - self.session, ba.DualTeamSession) else 'last one standing wins' + self.session, bs.DualTeamSession) else 'last one standing wins' def on_player_join(self, player: Player) -> None: @@ -275,9 +277,9 @@ def on_player_join(self, player: Player) -> None: if (self._get_total_team_lives(player.team) == 0 and player.team.survival_seconds is None): player.team.survival_seconds = 0 - ba.screenmessage( - ba.Lstr(resource='playerDelayedJoinText', - subs=[('${PLAYER}', player.getname(full=True))]), + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), color=(0, 1, 0), ) return @@ -293,11 +295,11 @@ def on_player_join(self, player: Player) -> None: def on_begin(self) -> None: super().on_begin() - self._start_time = ba.time() + self._start_time = bs.time() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() - self._vs_text = ba.NodeActor( - ba.newnode('text', + self._vs_text = bs.NodeActor( + bs.newnode('text', attrs={ 'position': (0, 92), 'h_attach': 'center', @@ -308,12 +310,12 @@ def on_begin(self) -> None: 'scale': 0.6, 'v_attach': 'bottom', 'color': (0.8, 0.8, 0.3, 1.0), - 'text': ba.Lstr(resource='vsText') + 'text': babase.Lstr(resource='vsText') })) # If balance-team-lives is on, add lives to the smaller team until # total lives match. - if (isinstance(self.session, ba.DualTeamSession) + if (isinstance(self.session, bs.DualTeamSession) and self._balance_total_lives and self.teams[0].players and self.teams[1].players): if self._get_total_team_lives( @@ -333,7 +335,7 @@ def on_begin(self) -> None: # We could check game-over conditions at explicit trigger points, # but lets just do the simple thing and poll it. - ba.timer(1.0, self._update, repeat=True) + bs.timer(1.0, self._update, repeat=True) def _update_alliance_mode(self) -> None: # For both teams, find the first player on the spawn order list with @@ -391,10 +393,10 @@ def _update_icons(self) -> None: nplayers -= 1 test_lives += 1 - def _get_spawn_point(self, player: Player) -> Optional[ba.Vec3]: + def _get_spawn_point(self, player: Player) -> Optional[babase.Vec3]: return None - def spawn_player(self, player: Player) -> ba.Actor: + def spawn_player(self, player: Player) -> bs.Actor: actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) # If we have any icons, update their state. @@ -403,7 +405,7 @@ def spawn_player(self, player: Player) -> ba.Actor: return actor def _print_lives(self, player: Player) -> None: - from bastd.actor import popuptext + from bascenev1lib.actor import popuptext # We get called in a timer so it's possible our player has left/etc. if not player or not player.is_alive() or not player.node: @@ -426,20 +428,20 @@ def on_player_leave(self, player: Player) -> None: # Update icons in a moment since our team will be gone from the # list then. - ba.timer(0, self._update_icons) + bs.timer(0, self._update_icons) # If the player to leave was the last in spawn order and had # their final turn currently in-progress, mark the survival time # for their team. if self._get_total_team_lives(player.team) == 0: assert self._start_time is not None - player.team.survival_seconds = int(ba.time() - self._start_time) + player.team.survival_seconds = int(bs.time() - self._start_time) def _get_total_team_lives(self, team: Team) -> int: return sum(player.lives for player in team.players) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) @@ -447,7 +449,7 @@ def handlemessage(self, msg: Any) -> Any: player.lives -= 1 if player.lives < 0: - ba.print_error( + babase.print_error( "Got lives < 0 in Alliance Elimination; this shouldn't happen.") player.lives = 0 @@ -458,14 +460,14 @@ def handlemessage(self, msg: Any) -> Any: # Play big death sound on our last death # or for every one. if player.lives == 0: - ba.playsound(SpazFactory.get().single_player_death_sound) + SpazFactory.get().single_player_death_sound.play() # If we hit zero lives, we're dead (and our team might be too). if player.lives == 0: # If the whole team is now dead, mark their survival time. if self._get_total_team_lives(player.team) == 0: assert self._start_time is not None - player.team.survival_seconds = int(ba.time() - + player.team.survival_seconds = int(bs.time() - self._start_time) # Put ourself at the back of the spawn order. @@ -493,7 +495,7 @@ def _update(self) -> None: # the game (allows the dust to settle and draws to occur if deaths # are close enough). if len(self._get_living_teams()) < 2: - self._round_end_timer = ba.Timer(0.5, self.end_game) + self._round_end_timer = bs.Timer(0.5, self.end_game) def _get_living_teams(self) -> List[Team]: return [ @@ -505,7 +507,7 @@ def _get_living_teams(self) -> List[Team]: def end_game(self) -> None: if self.has_ended(): return - results = ba.GameResults() + results = bs.GameResults() self._vs_text = None # Kill our 'vs' if its there. for team in self.teams: results.set_team_score(team, team.survival_seconds) diff --git a/plugins/minigames/arms_race.py b/plugins/minigames/arms_race.py new file mode 100644 index 000000000..cb797ddad --- /dev/null +++ b/plugins/minigames/arms_race.py @@ -0,0 +1,197 @@ +# Ported by your friend: Freaku + +# Join BCS: +# https://discord.gg/ucyaesh + + +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs + +if TYPE_CHECKING: + from typing import Any, Type, List, Union, Sequence + + +class State: + def __init__(self, bomb=None, grab=False, punch=False, curse=False, required=False, final=False, name=''): + self.bomb = bomb + self.grab = grab + self.punch = punch + self.pickup = False + self.curse = curse + self.required = required or final + self.final = final + self.name = name + self.next = None + self.index = None + + def apply(self, spaz): + spaz.disconnect_controls_from_player() + spaz.connect_controls_to_player(enable_punch=self.punch, + enable_bomb=self.bomb, + enable_pickup=self.grab) + if self.curse: + try: + spaz.curse_time = -1 + spaz.curse() + except: + pass + if self.bomb: + spaz.bomb_type = self.bomb + spaz.set_score_text(self.name) + + def get_setting(self): + return (self.name) + + +states = [State(bomb='normal', name='Basic Bombs'), + State(bomb='ice', name='Frozen Bombs'), + State(bomb='sticky', name='Sticky Bombs'), + State(bomb='impact', name='Impact Bombs'), + State(grab=True, name='Grabbing only'), + State(punch=True, name='Punching only'), + State(curse=True, name='Cursed', final=True)] + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self): + self.state = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class ArmsRaceGame(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Arms Race' + description = 'Upgrade your weapon by eliminating enemies.\nWin the match by being the first player\nto get a kill while cursed.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False)] + for state in states: + if not state.required: + settings.append(bs.BoolSetting(state.get_setting(), default=True)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self.states = [s for s in states if settings.get(s.name, True)] + for i, state in enumerate(self.states): + if i < len(self.states) and not state.final: + state.next = self.states[i + 1] + state.index = i + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._time_limit = float(settings['Time Limit']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Upgrade your weapon by eliminating enemies.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies', len(self.states) + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + + def on_player_join(self, player): + if player.state is None: + player.state = self.states[0] + self.spawn_player(player) + + # overriding the default character spawning.. + def spawn_player(self, player): + if player.state is None: + player.state = self.states[0] + super().spawn_player(player) + player.state.apply(player.actor) + + def isValidKill(self, m): + if m.getkillerplayer(Player) is None: + return False + + if m.getkillerplayer(Player).team is m.getplayer(Player).team: + return False + + return True + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + if self.isValidKill(msg): + self.stats.player_scored(msg.getkillerplayer(Player), 10, kill=True) + if not msg.getkillerplayer(Player).state.final: + msg.getkillerplayer(Player).state = msg.getkillerplayer(Player).state.next + msg.getkillerplayer(Player).state.apply(msg.getkillerplayer(Player).actor) + else: + msg.getkillerplayer(Player).team.score += 1 + self.end_game() + self.respawn_player(msg.getplayer(Player)) + + else: + return super().handlemessage(msg) + return None + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/plugins/minigames/avalanche.py b/plugins/minigames/avalanche.py new file mode 100644 index 000000000..3360c6ef6 --- /dev/null +++ b/plugins/minigames/avalanche.py @@ -0,0 +1,139 @@ +"""Avalancha mini-game.""" + +# ba_meta require api 9 + +from __future__ import annotations +import random +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.game.meteorshower import * +from bascenev1lib.actor.spazbot import * +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List, Type, Type + +## MoreMinigames.py support ## +randomPic = ["lakeFrigidPreview", "hockeyStadiumPreview"] + + +def ba_get_api_version(): + return 9 + + +def ba_get_levels(): + return [ + bs._level.Level( + "Icy Emits", + gametype=IcyEmitsGame, + settings={}, + preview_texture_name=random.choice(randomPic), + ) + ] + + +## MoreMinigames.py support ## + + +class PascalBot(BrawlerBot): + color = (0, 0, 3) + highlight = (0.2, 0.2, 1) + character = "Pascal" + bouncy = True + punchiness = 0.7 + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, bs.FreezeMessage): + return + else: + super().handlemessage(msg) + + +# ba_meta export bascenev1.GameActivity +class AvalanchaGame(MeteorShowerGame): + """Minigame involving dodging falling bombs.""" + + name = "Avalanche" + description = "Dodge the ice-bombs." + available_settings = [ + bs.BoolSetting("Epic Mode", default=False), + bs.IntSetting("Difficulty", default=1, min_value=1, max_value=3, increment=1), + ] + scoreconfig = bs.ScoreConfig( + label="Survived", scoretype=bs.ScoreType.MILLISECONDS, version="B" + ) + + announce_player_deaths = True + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ["Tip Top"] + + def __init__(self, settings: dict): + super().__init__(settings) + + self._epic_mode = settings.get("Epic Mode", False) + self._last_player_death_time: Optional[float] = None + self._meteor_time = 2.0 + if settings["Difficulty"] == 1: + self._min_delay = 0.4 + elif settings["Difficulty"] == 2: + self._min_delay = 0.3 + else: + self._min_delay = 0.1 + + self._timer: Optional[OnScreenTimer] = None + self._bots = SpazBotSet() + + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL + ) + if self._epic_mode: + self.slow_motion = True + + def on_transition_in(self) -> None: + super().on_transition_in() + gnode = bs.getactivity().globalsnode + gnode.tint = (0.5, 0.5, 1) + + act = bs.getactivity().map + shared = SharedObjects.get() + mat = bs.Material() + mat.add_actions(actions=("modify_part_collision", "friction", 0.18)) + + act.node.color = act.bottom.color = (1, 1, 1.2) + act.node.reflection = act.bottom.reflection = "soft" + act.node.materials = [shared.footing_material, mat] + + def _set_meteor_timer(self) -> None: + bs.timer( + (1.0 + 0.2 * random.random()) * self._meteor_time, self._drop_bomb_cluster + ) + + def _drop_bomb_cluster(self) -> None: + defs = self.map.defs + delay = 0.0 + for _i in range(random.randrange(1, 3)): + pos = defs.points["flag_default"] + pos = (pos[0], pos[1] + 0.4, pos[2]) + dropdir = -1.0 if pos[0] > 0 else 1.0 + vel = (random.randrange(-4, 4), 7.0, random.randrange(0, 4)) + bs.timer(delay, babase.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + def _drop_bomb(self, position: Sequence[float], velocity: Sequence[float]) -> None: + Bomb(position=position, velocity=velocity, bomb_type="ice").autoretain() + + def _decrement_meteor_time(self) -> None: + if self._meteor_time < self._min_delay: + return + self._meteor_time = max(0.01, self._meteor_time * 0.9) + if random.choice([0, 0, 1]) == 1: + pos = self.map.defs.points["flag_default"] + self._bots.spawn_bot(PascalBot, pos=pos, spawn_time=2) diff --git a/plugins/minigames/basket_bomb.py b/plugins/minigames/basket_bomb.py new file mode 100644 index 000000000..0fa83f18d --- /dev/null +++ b/plugins/minigames/basket_bomb.py @@ -0,0 +1,768 @@ +# Released under the MIT License. See LICENSE for details. +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor import playerspaz as ps +from bascenev1lib import maps + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + +bsuSpaz = None + + +def getlanguage(text, sub: str = ''): + lang = bs.app.lang.language + translate = { + "Name": + {"Spanish": "Baloncesto", + "English": "Basketbomb", + "Portuguese": "Basketbomb"}, + "Info": + {"Spanish": "Anota todas las canastas y sé el MVP.", + "English": "Score all the baskets and be the MVP.", + "Portuguese": "Marque cada cesta e seja o MVP."}, + "Info-Short": + {"Spanish": f"Anota {sub} canasta(s) para ganar", + "English": f"Score {sub} baskets to win", + "Portuguese": f"Cestas de {sub} pontos para ganhar"}, + "S: Powerups": + {"Spanish": "Aparecer Potenciadores", + "English": "Powerups Spawn", + "Portuguese": "Habilitar Potenciadores"}, + "S: Velocity": + {"Spanish": "Activar velocidad", + "English": "Enable speed", + "Portuguese": "Ativar velocidade"}, + } + + languages = ['Spanish', 'Portuguese', 'English'] + if lang not in languages: + lang = 'English' + + if text not in translate: + return text + return translate[text][lang] + + +class BallDiedMessage: + def __init__(self, ball: Ball): + self.ball = ball + + +class Ball(bs.Actor): + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + velocty = (0.0, 8.0, 0.0) + _scale = 1.2 + + self._spawn_pos = (position[0], position[1] + 0.5, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + + assert activity is not None + assert isinstance(activity, BasketGame) + + pmats = [shared.object_material, activity.ball_material] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': activity.ball_mesh, + 'color_texture': activity.ball_tex, + 'body': 'sphere', + 'reflection': 'soft', + 'body_scale': 1.0 * _scale, + 'reflection_scale': [1.3], + 'shadow_size': 1.0, + 'gravity_scale': 0.92, + 'density': max(0.4 * _scale, 0.3), + 'position': self._spawn_pos, + 'velocity': velocty, + 'materials': pmats}) + self.scale = scale = 0.25 * _scale + bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: scale*1.3, 0.26: scale}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(BallDiedMessage(self)) + + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + self.node.velocity = (0.0, 0.0, 0.0) + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +class Points: + postes = dict() + # 10.736066818237305, 0.3002409040927887, 0.5281256437301636 + postes['pal_0'] = (10.64702320098877, 0.0000000000000000, 0.0000000000000000) + postes['pal_1'] = (-10.64702320098877, 0.0000000000000000, 0.0000000000000000) + +# ba_meta export bascenev1.GameActivity + + +class BasketGame(bs.TeamGameActivity[Player, Team]): + + name = getlanguage('Name') + description = getlanguage('Info') + available_settings = [ + bs.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting(getlanguage('S: Powerups'), default=True), + bs.BoolSetting(getlanguage('S: Velocity'), default=False), + bs.BoolSetting('Epic Mode', default=False), + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['BasketBall Stadium', 'BasketBall Stadium V2'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = bs.getsound('cheer') + self._chant_sound = bs.getsound('crowdChant') + self._foghorn_sound = bs.getsound('foghorn') + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self.ball_mesh = bs.getmesh('shield') + self.ball_tex = bs.gettexture('fontExtras3') + self._ball_sound = bs.getsound('bunnyJump') + self._powerups = bool(settings[getlanguage('S: Powerups')]) + self._speed = bool(settings[getlanguage('S: Velocity')]) + self._epic_mode = bool(settings['Epic Mode']) + self.slow_motion = self._epic_mode + + self.ball_material = bs.Material() + self.ball_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.ball_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.ball_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.ball_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._ball_sound, 0.2, 5)) + + # Keep track of which player last touched the ball + self.ball_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_ball_player_collide), )) + + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.ball_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self._ball_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[bs.NodeActor]] = None + self._ball: Optional[Ball] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + return getlanguage('Info-Short', sub=self._score_to_win) + + def get_instance_description_short(self) -> Union[str, Sequence]: + return getlanguage('Info-Short', sub=self._score_to_win) + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + + if self._powerups: + self.setup_standard_powerup_drops() + + self._ball_spawn_pos = self.map.get_flag_position(None) + self._spawn_ball() + + defs = self.map.defs + self._score_regions = [] + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': defs.boxes['goal1'][0:3], + 'scale': defs.boxes['goal1'][6:9], + 'type': 'box', + 'materials': [] + }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': defs.boxes['goal2'][0:3], + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': [] + }))) + self._update_scoreboard() + self._chant_sound.play() + + for id, team in enumerate(self.teams): + self.postes(id) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_ball_player_collide(self) -> None: + collision = bs.getcollision() + try: + ball = collision.sourcenode.getdelegate(Ball, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + ball.last_players_to_touch[player.team.id] = player + + def _kill_ball(self) -> None: + self._ball = None + + def _handle_score(self, team_index: int = None) -> None: + assert self._ball is not None + assert self._score_regions is not None + + if self._ball.scored: + return + + region = bs.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + if team_index is not None: + index = team_index + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) + + if (scoring_team.id in self._ball.last_players_to_touch + and self._ball.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._ball.last_players_to_touch[scoring_team.id], + 100, big_message=True) + + if team.score >= self._score_to_win: + self.end_game() + + # self._foghorn_sound.play() + self._cheer_sound.play() + + self._ball.scored = True + + # Kill the ball (it'll respawn itself shortly). + bs.timer(1.0, self._kill_ball) + + light = bs.newnode('light', + attrs={ + 'position': bs.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + bs.timer(1.0, light.delete) + + bs.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for id, team in enumerate(self.teams): + self._scoreboard.set_team_value(team, team.score, winscore) + # self.postes(id) + + def spawn_player(self, player: Player) -> bs.Actor: + if bsuSpaz is None: + spaz = self.spawn_player_spaz(player) + else: + ps.PlayerSpaz = bsuSpaz.BskSpaz + spaz = self.spawn_player_spaz(player) + ps.PlayerSpaz = bsuSpaz.OldPlayerSpaz + + if self._speed: + spaz.node.hockey = True + return spaz + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + elif isinstance(msg, BallDiedMessage): + if not self.has_ended(): + bs.timer(3.0, self._spawn_ball) + else: + super().handlemessage(msg) + + def postes(self, team_id: int): + if not hasattr(self._map, 'poste_'+str(team_id)): + setattr(self._map, 'poste_'+str(team_id), + Palos(team=team_id, + position=Points.postes['pal_' + + str(team_id)]).autoretain()) + + def _flash_ball_spawn(self) -> None: + light = bs.newnode('light', + attrs={ + 'position': self._ball_spawn_pos, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) + + def _spawn_ball(self) -> None: + self._swipsound.play() + self._whistle_sound.play() + self._flash_ball_spawn() + assert self._ball_spawn_pos is not None + self._ball = Ball(position=self._ball_spawn_pos) + + +class Aro(bs.Actor): + def __init__(self, team: int = 0, + position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + act = self.getactivity() + shared = SharedObjects.get() + setattr(self, 'team', team) + setattr(self, 'locs', []) + + # Material Para; Traspasar Objetos + self.no_collision = bs.Material() + self.no_collision.add_actions( + actions=(('modify_part_collision', 'collide', False))) + + self.collision = bs.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + # Score + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', act.ball_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._annotation))) + + self._spawn_pos = (position[0], position[1], position[2]) + self._materials_region0 = [self.collision, + shared.footing_material] + + mesh = None + tex = bs.gettexture('null') + + pmats = [self.no_collision] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': mesh, + 'color_texture': tex, + 'body': 'box', + 'reflection': 'soft', + 'reflection_scale': [1.5], + 'shadow_size': 0.1, + 'position': self._spawn_pos, + 'materials': pmats}) + + self.scale = scale = 1.4 + bs.animate(self.node, 'mesh_scale', {0: 0}) + + pos = (position[0], position[1]+0.6, position[2]) + self.regions: List[bs.Node] = [ + bs.newnode('region', + attrs={'position': position, + 'scale': (0.6, 0.05, 0.6), + 'type': 'box', + 'materials': self._materials_region0}), + + bs.newnode('region', + attrs={'position': pos, + 'scale': (0.5, 0.3, 0.9), + 'type': 'box', + 'materials': [self._score_region_material]}) + ] + self.regions[0].connectattr('position', self.node, 'position') + # self.regions[0].connectattr('position', self.regions[1], 'position') + + locs_count = 9 + pos = list(position) + + try: + id = 0 if team == 1 else 1 + color = act.teams[id].color + except: + color = (1, 1, 1) + + while locs_count > 1: + scale = (1.5 * 0.1 * locs_count) + 0.8 + + self.locs.append(bs.newnode('locator', + owner=self.node, + attrs={'shape': 'circleOutline', + 'position': pos, + 'color': color, + 'opacity': 1.0, + 'size': [scale], + 'draw_beauty': True, + 'additive': False})) + + pos[1] -= 0.1 + locs_count -= 1 + + def _annotation(self): + assert len(self.regions) >= 2 + ball = self.getactivity()._ball + + if ball: + p = self.regions[0].position + ball.node.position = p + ball.node.velocity = (0.0, 0.0, 0.0) + + act = self.getactivity() + act._handle_score(self.team) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if self.node.exists(): + self.node.delete() + else: + super().handlemessage(msg) + + +class Cuadro(bs.Actor): + def __init__(self, team: int = 0, + position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + act = self.getactivity() + shared = SharedObjects.get() + setattr(self, 'locs', []) + + self.collision = bs.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + pos = (position[0], position[1]+0.9, position[2]+1.5) + self.region: bs.Node = bs.newnode('region', + attrs={'position': pos, + 'scale': (0.5, 2.7, 2.5), + 'type': 'box', + 'materials': [self.collision, + shared.footing_material]}) + + # self.shield = bs.newnode('shield', attrs={'radius': 1.0, 'color': (0,10,0)}) + # self.region.connectattr('position', self.shield, 'position') + + position = (position[0], position[1], position[2]+0.09) + pos = list(position) + oldpos = list(position) + old_count = 14 + + count = old_count + count_y = 9 + + try: + id = 0 if team == 1 else 1 + color = act.teams[id].color + except: + color = (1, 1, 1) + + while (count_y != 1): + + while (count != 1): + pos[2] += 0.19 + + self.locs.append( + bs.newnode('locator', + owner=self.region, + attrs={'shape': 'circle', + 'position': pos, + 'size': [0.5], + 'color': color, + 'opacity': 1.0, + 'draw_beauty': True, + 'additive': False})) + count -= 1 + + count = old_count + pos[1] += 0.2 + pos[2] = oldpos[2] + count_y -= 1 + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if self.node.exists(): + self.node.delete() + else: + super().handlemessage(msg) + + +class Palos(bs.Actor): + def __init__(self, team: int = 0, + position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + self._pos = position + self.aro = None + self.cua = None + + # Material Para; Traspasar Objetos + self.no_collision = bs.Material() + self.no_collision.add_actions( + actions=(('modify_part_collision', 'collide', False))) + + # + self.collision = bs.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[2]+2.5, position[2]) + + mesh = bs.getmesh('flagPole') + tex = bs.gettexture('flagPoleColor') + + pmats = [self.no_collision] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': mesh, + 'color_texture': tex, + 'body': 'puck', + 'reflection': 'soft', + 'reflection_scale': [2.6], + 'shadow_size': 0, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + self.scale = scale = 4.0 + bs.animate(self.node, 'mesh_scale', {0: scale}) + + self.loc = bs.newnode('locator', + owner=self.node, + attrs={'shape': 'circle', + 'position': position, + 'color': (1, 1, 0), + 'opacity': 1.0, + 'draw_beauty': False, + 'additive': True}) + + self._y = _y = 0.30 + _x = -0.25 if team == 1 else 0.25 + _pos = (position[0]+_x, position[1]-1.5 + _y, position[2]) + self.region = bs.newnode('region', + attrs={ + 'position': _pos, + 'scale': (0.4, 8, 0.4), + 'type': 'box', + 'materials': [self.collision]}) + self.region.connectattr('position', self.node, 'position') + + _y = self._y + position = self._pos + if team == 0: + pos = (position[0]-0.8, position[1] + 2.0 + _y, position[2]) + else: + pos = (position[0]+0.8, position[1] + 2.0 + _y, position[2]) + + if self.aro is None: + self.aro = Aro(team, pos).autoretain() + + if self.cua is None: + pos = (position[0], position[1] + 1.8 + _y, position[2]-1.4) + self.cua = Cuadro(team, pos).autoretain() + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if self.node.exists(): + self.node.delete() + else: + super().handlemessage(msg) + + +class BasketMap(maps.FootballStadium): + name = 'BasketBall Stadium' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [] + + def __init__(self) -> None: + super().__init__() + + gnode = bs.getactivity().globalsnode + gnode.tint = [(0.806, 0.8, 1.0476), (1.3, 1.2, 1.0)][0] + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + + +class BasketMapV2(maps.HockeyStadium): + name = 'BasketBall Stadium V2' + + def __init__(self) -> None: + super().__init__() + + shared = SharedObjects.get() + self.node.materials = [shared.footing_material] + self.node.collision_mesh = bs.getcollisionmesh('footballStadiumCollide') + self.node.mesh = None + self.stands.mesh = None + self.floor.reflection = 'soft' + self.floor.reflection_scale = [1.6] + self.floor.color = (1.1, 0.05, 0.8) + + self.background = bs.newnode('terrain', + attrs={'mesh': bs.getmesh('thePadBG'), + 'lighting': False, + 'background': True, + 'color': (1.0, 0.2, 1.0), + 'color_texture': bs.gettexture('menuBG')}) + + gnode = bs.getactivity().globalsnode + gnode.floor_reflection = True + gnode.debris_friction = 0.3 + gnode.debris_kill_height = -0.3 + gnode.tint = [(1.2, 1.3, 1.33), (0.7, 0.9, 1.0)][1] + gnode.ambient_color = (1.15, 1.25, 1.6) + gnode.vignette_outer = (0.66, 0.67, 0.73) + gnode.vignette_inner = (0.93, 0.93, 0.95) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + self.is_hockey = False + + ################## + self.collision = bs.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + self.regions: List[bs.Node] = [ + bs.newnode('region', + attrs={'position': (12.676897048950195, 0.2997918128967285, 5.583303928375244), + 'scale': (1.01, 12, 28), + 'type': 'box', + 'materials': [self.collision]}), + + bs.newnode('region', + attrs={'position': (11.871315956115723, 0.29975247383117676, 5.711406707763672), + 'scale': (50, 12, 0.9), + 'type': 'box', + 'materials': [self.collision]}), + + bs.newnode('region', + attrs={'position': (-12.776557922363281, 0.30036890506744385, 4.96237850189209), + 'scale': (1.01, 12, 28), + 'type': 'box', + 'materials': [self.collision]}), + ] + + +bs._map.register_map(BasketMap) +bs._map.register_map(BasketMapV2) diff --git a/plugins/minigames/better_deathmatch.py b/plugins/minigames/better_deathmatch.py new file mode 100644 index 000000000..4883a538f --- /dev/null +++ b/plugins/minigames/better_deathmatch.py @@ -0,0 +1,268 @@ +# BetterDeathMatch +# Made by your friend: @[Just] Freak#4999 + +"""Defines a very-customisable DeathMatch mini-game""" + +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Type, List, Union, Sequence, Optional + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class BetterDeathMatchGame(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Btrr Death Match' + description = 'Kill a set number of enemies to win.\nbyFREAK' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + + + ## Add settings ## + bs.BoolSetting('Enable Gloves', False), + bs.BoolSetting('Enable Powerups', True), + bs.BoolSetting('Night Mode', False), + bs.BoolSetting('Icy Floor', False), + bs.BoolSetting('One Punch Kill', False), + bs.BoolSetting('Spawn with Shield', False), + bs.BoolSetting('Punching Only', False), + ## Add settings ## + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = bui.getsound('dingSmall') + + +## Take applied settings ## + self._boxing_gloves = bool(settings['Enable Gloves']) + self._enable_powerups = bool(settings['Enable Powerups']) + self._night_mode = bool(settings['Night Mode']) + self._icy_floor = bool(settings['Icy Floor']) + self._one_punch_kill = bool(settings['One Punch Kill']) + self._shield_ = bool(settings['Spawn with Shield']) + self._only_punch = bool(settings['Punching Only']) +## Take applied settings ## + + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Crush ${ARG1} of your enemies. byFREAK', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies. byFREAK', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + +## Run settings related: IcyFloor ## + + + def on_transition_in(self) -> None: + super().on_transition_in() + activity = bs.getactivity() + if self._icy_floor: + activity.map.is_hockey = True + else: + return +## Run settings related: IcyFloor ## + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + + +## Run settings related: NightMode,Powerups ## + if self._night_mode: + bs.getactivity().globalsnode.tint = (0.5, 0.7, 1) + else: + pass +# -# Tried return here, pfft. Took me 30mins to figure out why pwps spawning only on NightMode +# -# Now its fixed :) + if self._enable_powerups: + self.setup_standard_powerup_drops() + else: + pass +## Run settings related: NightMode,Powerups ## + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + self._dingsound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + self._dingsound.play() + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + +## Run settings related: Spaz ## + + + def spawn_player(self, player: Player) -> bs.Actor: + spaz = self.spawn_player_spaz(player) + if self._boxing_gloves: + spaz.equip_boxing_gloves() + if self._one_punch_kill: + spaz._punch_power_scale = 15 + if self._shield_: + spaz.equip_shields() + if self._only_punch: + spaz.connect_controls_to_player(enable_bomb=False, enable_pickup=False) + + return spaz +## Run settings related: Spaz ## + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/plugins/minigames/better_elimination.py b/plugins/minigames/better_elimination.py new file mode 100644 index 000000000..bac9c20dc --- /dev/null +++ b/plugins/minigames/better_elimination.py @@ -0,0 +1,658 @@ +# BetterElimination +# Made by your friend: @[Just] Freak#4999 + +# Huge Thx to Nippy for "Live Team Balance" + + +"""Defines a very-customisable Elimination mini-game""" + +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import (Any, Tuple, Type, List, Sequence, Optional, + Union) + + +class Icon(bs.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: Tuple[float, float], + scale: float, + show_lives: bool = True, + show_death: bool = True, + name_scale: float = 1.0, + name_maxwidth: float = 115.0, + flatness: float = 1.0, + shadow: float = 1.0): + super().__init__() + + self._player = player + self._show_lives = show_lives + self._show_death = show_death + self._name_scale = name_scale + self._outline_tex = bs.gettexture('characterIconMask') + + icon = player.get_icon() + self.node = bs.newnode('image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + self._name_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': flatness, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + if self._show_lives: + self._lives_text = bs.newnode('text', + owner=self.node, + attrs={ + 'text': 'x0', + 'color': (1, 1, 0.5), + 'h_align': 'left', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_position_and_scale(position, scale) + + def set_position_and_scale(self, position: Tuple[float, float], + scale: float) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + if self._show_lives: + self._lives_text.position = (position[0] + scale * 10.0, + position[1] - scale * 43.0) + self._lives_text.scale = 1.0 * scale + + def update_for_lives(self) -> None: + """Update for the target player's current lives.""" + if self._player: + lives = self._player.lives + else: + lives = 0 + if self._show_lives: + if lives > 0: + self._lives_text.text = 'x' + str(lives - 1) + else: + self._lives_text.text = '' + if lives == 0: + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + def handle_player_spawned(self) -> None: + """Our player spawned; hooray!""" + if not self.node: + return + self.node.opacity = 1.0 + self.update_for_lives() + + def handle_player_died(self) -> None: + """Well poo; our player died.""" + if not self.node: + return + if self._show_death: + bs.animate( + self.node, 'opacity', { + 0.00: 1.0, + 0.05: 0.0, + 0.10: 1.0, + 0.15: 0.0, + 0.20: 1.0, + 0.25: 0.0, + 0.30: 1.0, + 0.35: 0.0, + 0.40: 1.0, + 0.45: 0.0, + 0.50: 1.0, + 0.55: 0.2 + }) + lives = self._player.lives + if lives == 0: + bs.timer(0.6, self.update_for_lives) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + self.node.delete() + return None + return super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.lives = 0 + self.icons: List[Icon] = [] + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: Optional[int] = None + self.spawn_order: List[Player] = [] + + +# ba_meta export bascenev1.GameActivity +class BetterEliminationGame(bs.TeamGameActivity[Player, Team]): + """Game type where last player(s) left alive win.""" + + name = 'Bttr Elimination' + description = 'Last remaining alive wins.\nbyFREAK' + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, + none_is_winner=True) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + 'Life\'s Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + + + ## Add settings ## + bs.BoolSetting('Live Team Balance (by Nippy#2677)', True), + bs.BoolSetting('Enable Gloves', False), + bs.BoolSetting('Enable Powerups', True), + bs.BoolSetting('Night Mode', False), + bs.BoolSetting('Icy Floor', False), + bs.BoolSetting('One Punch Kill', False), + bs.BoolSetting('Spawn with Shield', False), + bs.BoolSetting('Punching Only', False), + ## Add settings ## + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Life\'s (on spawn only)', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._start_time: Optional[float] = None + self._vs_text: Optional[bs.Actor] = None + self._round_end_timer: Optional[bs.Timer] = None + +## Take applied settings ## + self._live_team_balance = bool(settings['Live Team Balance (by Nippy#2677)']) + self._boxing_gloves = bool(settings['Enable Gloves']) + self._enable_powerups = bool(settings['Enable Powerups']) + self._night_mode = bool(settings['Night Mode']) + self._icy_floor = bool(settings['Icy Floor']) + self._one_punch_kill = bool(settings['One Punch Kill']) + self._shield_ = bool(settings['Spawn with Shield']) + self._only_punch = bool(settings['Punching Only']) +## Take applied settings ## + + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = int(settings['Life\'s Per Player']) + self._time_limit = float(settings['Time Limit']) + self._balance_total_lives = bool( + settings.get('Balance Total Life\'s (on spawn only)', False)) + self._solo_mode = bool(settings.get('Solo Mode', False)) + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Last team standing wins. byFREAK' if isinstance( + self.session, bs.DualTeamSession) else 'Last one standing wins.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'last team standing wins. byFREAK' if isinstance( + self.session, bs.DualTeamSession) else 'last one standing wins' + + def on_player_join(self, player: Player) -> None: + + # No longer allowing mid-game joiners here; too easy to exploit. + if self.has_begun(): + + # Make sure their team has survival seconds set if they're all dead + # (otherwise blocked new ffa players are considered 'still alive' + # in score tallying). + if (self._get_total_team_lives(player.team) == 0 + and player.team.survival_seconds is None): + player.team.survival_seconds = 0 + bui.screenmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + return + + player.lives = self._lives_per_player + + if self._solo_mode: + player.team.spawn_order.append(player) + self._update_solo_mode() + else: + # Create our icon and spawn. + player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: + self.spawn_player(player) + + # Don't waste time doing this until begin. + if self.has_begun(): + self._update_icons() + + +## Run settings related: IcyFloor ## + + + def on_transition_in(self) -> None: + super().on_transition_in() + activity = bs.getactivity() + if self._icy_floor: + activity.map.is_hockey = True + else: + return +## Run settings related: IcyFloor ## + + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.setup_standard_time_limit(self._time_limit) + + +## Run settings related: NightMode,Powerups ## + if self._night_mode: + bs.getactivity().globalsnode.tint = (0.5, 0.7, 1) + else: + pass +# -# Tried return here, pfft. Took me 30mins to figure out why pwps spawning only on NightMode +# -# Now its fixed :) + if self._enable_powerups: + self.setup_standard_powerup_drops() + else: + pass +## Run settings related: NightMode,Powerups ## + + if self._solo_mode: + self._vs_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': babase.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, bs.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + bs.timer(1.0, self._update, repeat=True) + + def _update_solo_mode(self) -> None: + # For both teams, find the first player on the spawn order list with + # lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + break + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon(player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False)) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def _get_spawn_point(self, player: Player) -> Optional[babase.Vec3]: + del player # Unused. + + # In solo-mode, if there's an existing live player on the map, spawn at + # whichever spot is farthest from them (keeps the action spread out). + if self._solo_mode: + living_player = None + living_player_pos = None + for team in self.teams: + for tplayer in team.players: + if tplayer.is_alive(): + assert tplayer.node + ppos = tplayer.node.position + living_player = tplayer + living_player_pos = ppos + break + if living_player: + assert living_player_pos is not None + player_pos = babase.Vec3(living_player_pos) + points: List[Tuple[float, babase.Vec3]] = [] + for team in self.teams: + start_pos = babase.Vec3(self.map.get_start_position(team.id)) + points.append( + ((start_pos - player_pos).length(), start_pos)) + # Hmm.. we need to sorting vectors too? + points.sort(key=lambda x: x[0]) + return points[-1][1] + return None + + def spawn_player(self, player: Player) -> bs.Actor: + actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + +## Run settings related: Spaz ## + if self._boxing_gloves: + actor.equip_boxing_gloves() + if self._one_punch_kill: + actor._punch_power_scale = 15 + if self._shield_: + actor.equip_shields() + if self._only_punch: + actor.connect_controls_to_player(enable_bomb=False, enable_pickup=False) + + return actor +## Run settings related: Spaz ## + + def _print_lives(self, player: Player) -> None: + from bascenev1lib.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + popuptext.PopupText('x' + str(player.lives - 1), + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=player.node.position).autoretain() + + def on_player_leave(self, player: Player) -> None: + # Nippy#2677 + team_count = 1 # Just initiating + if player.lives > 0 and self._live_team_balance: + team_mem = [] + for teamer in player.team.players: + if player != teamer: + team_mem.append(teamer) # Got Dead players Team + live = player.lives + team_count = len(team_mem) + # Extending Player List for Sorted Players + for i in range(int((live if live % 2 == 0 else live+1)/2)): + team_mem.extend(team_mem) + if team_count > 0: + for i in range(live): + team_mem[i].lives += 1 + + if team_count <= 0: # Draw if Player Leaves + self.end_game() + # Nippy#2677 + super().on_player_leave(player) + player.icons = [] + + # Remove us from spawn-order. + if self._solo_mode: + if player in player.team.spawn_order: + player.team.spawn_order.remove(player) + + # Update icons in a moment since our team will be gone from the + # list then. + bs.timer(0, self._update_icons) + + # If the player to leave was the last in spawn order and had + # their final turn currently in-progress, mark the survival time + # for their team. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - self._start_time) + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + babase.print_error( + "Got lives < 0 in Elim; this shouldn't happen. solo:" + + str(self._solo_mode)) + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if self._solo_mode or player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + # If the whole team is now dead, mark their survival time. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - + self._start_time) + else: + # Otherwise, in regular mode, respawn. + if not self._solo_mode: + self.respawn_player(player) + + # In solo, put ourself at the back of the spawn order. + if self._solo_mode: + player.team.spawn_order.remove(player) + player.team.spawn_order.append(player) + + def _update(self) -> None: + if self._solo_mode: + # For both teams, find the first player on the spawn order + # list with lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + self._update_icons() + break + + # If we're down to 1 or fewer living teams, start a timer to end + # the game (allows the dust to settle and draws to occur if deaths + # are close enough). + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0.5, self.end_game) + + def _get_living_teams(self) -> List[Team]: + return [ + team for team in self.teams + if len(team.players) > 0 and any(player.lives > 0 + for player in team.players) + ] + + def end_game(self) -> None: + if self.has_ended(): + return + results = bs.GameResults() + self._vs_text = None # Kill our 'vs' if its there. + for team in self.teams: + results.set_team_score(team, team.survival_seconds) + self.end(results=results) diff --git a/plugins/minigames/big_ball.py b/plugins/minigames/big_ball.py new file mode 100644 index 000000000..e1a5d3a3f --- /dev/null +++ b/plugins/minigames/big_ball.py @@ -0,0 +1,518 @@ +# Made by MythB + +# ba_meta require api 9 +from __future__ import annotations +from typing import TYPE_CHECKING +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.gameutils import SharedObjects +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + +# goalpost + + +class FlagKale(bs.Actor): + def __init__(self, position=(0, 2.5, 0), color=(1, 1, 1)): + super().__init__() + activity = self.getactivity() + shared = SharedObjects.get() + self.node = bs.newnode('flag', + attrs={'position': (position[0], position[1]+0.75, position[2]), + 'color_texture': activity._flagKaleTex, + 'color': color, + 'materials': [shared.object_material, activity._kaleMaterial], + }, + delegate=self) + + def handleMessage(self, m): + if isinstance(m, bs.DieMessage): + if self.node.exists(): + self.node.delete() + elif isinstance(m, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + else: + super().handlemessage(m) + + +class Puck(bs.Actor): + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + assert activity is not None + assert isinstance(activity, BBGame) + pmats = [shared.object_material, activity.puck_material] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': activity._ballModel, + 'color_texture': activity._ballTex, + 'body': 'sphere', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.8, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats, + 'body_scale': 4, + 'mesh_scale': 1, + 'density': 0.02}) + bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + +# for night mode: using a actor with large shadow and little mesh scale. Better then tint i think, players and objects more visible + + +class NightMod(bs.Actor): + def __init__(self, position=(0, 0, 0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + # spawn just above the provided point + self._spawnPos = (position[0], position[1], position[2]) + self.node = bs.newnode("prop", + attrs={'mesh': activity._nightModel, + 'color_texture': activity._nightTex, + 'body': 'sphere', + 'reflection': 'soft', + 'body_scale': 0.1, + 'mesh_scale': 0.001, + 'density': 0.010, + 'reflection_scale': [0.23], + 'shadow_size': 999999.0, + 'is_area_of_interest': True, + 'position': self._spawnPos, + 'materials': [activity._nightMaterial] + }, + delegate=self) + + def handlemssage(self, m): + super().handlemessage(m) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class BBGame(bs.TeamGameActivity[Player, Team]): + name = 'Big Ball' + description = 'Score some goals.\nFlags are goalposts.\nScored team players get boxing gloves,\nNon-scored team players getting shield (if Grant Powers on Score).\nYou can also set Night Mode!' + available_settings = [ + bs.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', True), + bs.BoolSetting('Night Mode', False), + bs.BoolSetting('Grant Powers on Score', False) + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = bs.getsound('cheer') + self._chant_sound = bs.getsound('crowdChant') + self._foghorn_sound = bs.getsound('foghorn') + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self._ballModel = bs.getmesh("shield") + self._ballTex = bs.gettexture("eggTex1") + self._ballSound = bs.getsound("impactMedium2") + self._flagKaleTex = bs.gettexture("star") + self._kaleSound = bs.getsound("metalHit") + self._nightModel = bs.getmesh("shield") + self._nightTex = bs.gettexture("black") + self._kaleMaterial = bs.Material() + # add friction to flags for standing our position (as far as) + self._kaleMaterial.add_actions(conditions=("they_have_material", shared.footing_material), + actions=(("modify_part_collision", "friction", 9999.5))) + self._kaleMaterial.add_actions(conditions=(("we_are_younger_than", 1), 'and', + ("they_have_material", shared.object_material)), + actions=(("modify_part_collision", "collide", False))) + self._kaleMaterial.add_actions(conditions=("they_have_material", shared.pickup_material), + actions=(("modify_part_collision", "collide", False))) + self._kaleMaterial.add_actions( + conditions=('they_have_material', shared.object_material), + actions=(('impact_sound', self._kaleSound, 2, 5))) + # we dont wanna hit the night so + self._nightMaterial = bs.Material() + self._nightMaterial.add_actions(conditions=(('they_have_material', shared.pickup_material), 'or', + ('they_have_material', shared.attack_material)), + actions=(('modify_part_collision', 'collide', False))) + # we also dont want anything moving it + self._nightMaterial.add_actions( + conditions=(('they_have_material', shared.object_material), 'or', + ('they_dont_have_material', shared.footing_material)), + actions=(('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False))) + self.puck_material = bs.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', False)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.puck_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._ballSound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', bs.DieMessage()))) + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[bs.NodeActor]] = None + self._puck: Optional[Puck] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + self._nm = bool(settings['Night Mode']) + self._grant_power = bool(settings['Grant Powers on Score']) + self._epic_mode = bool(settings['Epic Mode']) + # Base class overrides. + self.slow_motion = self._epic_mode + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Score a goal.' + return 'Score ${ARG1} goals.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'score a goal' + return 'score ${ARG1} goals', self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops(enable_tnt=False) + self._puck_spawn_pos = self.map.get_flag_position(None) + self._spawn_puck() + # for night mode we need night actor. And same goodies for nigh mode + if self._nm: + self._nightSpawny(), self._flagKaleFlash() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (13.75, 0.85744967453, 0.1095578275), + 'scale': (1.05, 1.1, 3.8), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (-13.55, 0.85744967453, 0.1095578275), + 'scale': (1.05, 1.1, 3.8), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._update_scoreboard() + self._chant_sound.play() + + def _nightSpawny(self): + self.MythBrk = NightMod(position=(0, 0.05744967453, 0)) + + # spawn some goodies on nightmode for pretty visuals + def _flagKaleFlash(self): + # flags positions + kale1 = (-12.45, 0.05744967453, -2.075) + kale2 = (-12.45, 0.05744967453, 2.075) + kale3 = (12.66, 0.03986567039, 2.075) + kale4 = (12.66, 0.03986567039, -2.075) + + flash = bs.newnode("light", + attrs={'position': kale1, + 'radius': 0.15, + 'color': (1.0, 1.0, 0.7)}) + + flash = bs.newnode("light", + attrs={'position': kale2, + 'radius': 0.15, + 'color': (1.0, 1.0, 0.7)}) + + flash = bs.newnode("light", + attrs={'position': kale3, + 'radius': 0.15, + 'color': (0.7, 1.0, 1.0)}) + + flash = bs.newnode("light", + attrs={'position': kale4, + 'radius': 0.15, + 'color': (0.7, 1.0, 1.0)}) + # flags positions + + def _flagKalesSpawn(self): + for team in self.teams: + if team.id == 0: + _colorTeam0 = team.color + if team.id == 1: + _colorTeam1 = team.color + + self._MythB = FlagKale(position=(-12.45, 0.05744967453, -2.075), color=_colorTeam0) + self._MythB2 = FlagKale(position=(-12.45, 0.05744967453, 2.075), color=_colorTeam0) + self._MythB3 = FlagKale(position=(12.66, 0.03986567039, 2.075), color=_colorTeam1) + self._MythB4 = FlagKale(position=(12.66, 0.03986567039, -2.075), color=_colorTeam1) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = bs.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + puck.last_players_to_touch[player.team.id] = player + + def _kill_puck(self) -> None: + self._puck = None + + def _handle_score(self) -> None: + """A point has been scored.""" + + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = bs.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + # tell scored team players to celebrate and give them to boxing gloves + if self._grant_power: + for player in team.players: + try: + player.actor.node.handlemessage(bs.PowerupMessage('punch')) + except: + pass + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 100, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + else: + if self._grant_power: + for player in team.players: + try: + player.actor.node.handlemessage(bs.PowerupMessage('shield')) + except: + pass + + self._foghorn_sound.play() + self._cheer_sound.play() + + self._puck.scored = True + + # Kill the puck (it'll respawn itself shortly). + bs.timer(1.0, self._kill_puck) + + light = bs.newnode('light', + attrs={ + 'position': bs.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + bs.timer(1.0, light.delete) + + bs.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + bs.timer(3.0, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + light = bs.newnode('light', + attrs={ + 'position': self._puck_spawn_pos, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) + + def _spawn_puck(self) -> None: + self._swipsound.play() + self._whistle_sound.play() + self._flagKalesSpawn() + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) + self._puck.light = bs.newnode('light', + owner=self._puck.node, + attrs={'intensity': 0.3, + 'height_attenuated': False, + 'radius': 0.2, + 'color': (0.9, 0.2, 0.9)}) + self._puck.node.connectattr('position', self._puck.light, 'position') diff --git a/plugins/minigames/bomb_on_my_head.py b/plugins/minigames/bomb_on_my_head.py new file mode 100644 index 000000000..633d2f1dc --- /dev/null +++ b/plugins/minigames/bomb_on_my_head.py @@ -0,0 +1,337 @@ +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +import random +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.actor.spaz import BombDiedMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor import bomb as stdbomb + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = bs.app.lang.language + +if lang == 'Spanish': + name = 'Bomba en mi Cabeza' + description = ('Siempre tendrás una bomba en la cabeza.\n' + '¡Sobrevive tanto como puedas!') + description_ingame = '¡Sobrevive tanto como puedas!' + # description_short = 'Elimina {} Jugadores para ganar' + maxbomblimit = 'Límite Máximo de Bombas' + mbltwo = 'Dos' + mblthree = 'Tres' + mblfour = 'Cuatro' +else: + name = 'Bomb on my Head' + description = ('You\'ll always have a bomb on your head.\n' + 'Survive as long as you can!') + description_ingame = 'Survive as long as you can!' + # description_short = 'Kill {} Players to win' + maxbomblimit = 'Max Bomb Limit' + mbltwo = 'Two' + mblthree = 'Three' + mblfour = 'Four' + + +class NewPlayerSpaz(PlayerSpaz): + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, BombDiedMessage): + self.bomb_count += 1 + self.check_avalible_bombs() + else: + return super().handlemessage(msg) + + def check_avalible_bombs(self) -> None: + if not self.node: + return + if self.bomb_count <= 0: + return + if not self.node.hold_node: + self.on_bomb_press() + self.on_bomb_release() + + def start_bomb_checking(self) -> None: + self.check_avalible_bombs() + self._bomb_check_timer = bs.timer( + 0.5, + bs.WeakCall(self.check_avalible_bombs), + repeat=True) + + def drop_bomb(self) -> stdbomb.Bomb | None: + lifespan = 3.0 + + if self.bomb_count <= 0 or self.frozen: + return None + assert self.node + pos = self.node.position_forward + vel = self.node.velocity + + bomb_type = 'normal' + + bomb = stdbomb.Bomb( + position=(pos[0], pos[1] - 0.0, pos[2]), + velocity=(vel[0], vel[1], vel[2]), + bomb_type=bomb_type, + blast_radius=self.blast_radius, + source_player=self.source_player, + owner=self.node, + ).autoretain() + + bs.animate(bomb.node, 'mesh_scale', { + 0.0: 0.0, + lifespan*0.1: 1.5, + lifespan*0.5: 1.0 + }) + + self.bomb_count -= 1 + bomb.node.add_death_action( + bs.WeakCall(self.handlemessage, BombDiedMessage()) + ) + self._pick_up(bomb.node) + + for clb in self._dropped_bomb_callbacks: + clb(self, bomb) + + return bomb + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: float | None = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class BombOnMyHeadGame(bs.TeamGameActivity[Player, Team]): + + name = name + description = description + scoreconfig = bs.ScoreConfig( + label='Survived', scoretype=bs.ScoreType.MILLISECONDS, version='B' + ) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[babase.Setting]: + settings = [ + bs.IntChoiceSetting( + maxbomblimit, + choices=[ + ('Normal', 1), + (mbltwo, 2), + (mblthree, 3), + (mblfour, 4), + ], + default=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._max_bomb_limit = int(settings[maxbomblimit]) + self._epic_mode = bool(settings['Epic Mode']) + self._time_limit = float(settings['Time Limit']) + self._last_player_death_time: float | None = None + self._timer: OnScreenTimer | None = None + + # Some base class overrides: + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL + ) + if self._epic_mode: + self.slow_motion = True + + def get_instance_description(self) -> str | Sequence: + return description_ingame + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self._timer = OnScreenTimer() + self._timer.start() + + def spawn_player(self, player: Player) -> bs.Actor: + from babase import _math + from bascenev1._gameutils import animate + from bascenev1._coopsession import CoopSession + + if isinstance(self.session, bs.DualTeamSession): + position = self.map.get_start_position(player.team.id) + else: + # otherwise do free-for-all spawn locations + position = self.map.get_ffa_start_position(self.players) + angle = None + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = babase.safecolor(color, target_intensity=0.75) + + spaz = NewPlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + bs.timer(1.0, bs.WeakCall(spaz.start_bomb_checking)) + spaz.set_bomb_count(self._max_bomb_limit) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = bs.time() + + # Record the player's moment of death. + # assert isinstance(msg.spaz.player + msg.getplayer(Player).death_time = curtime + + # In co-op mode, end the game the instant everyone dies + # (more accurate looking). + # In teams/ffa, allow a one-second fudge-factor so we can + # get more draws if players die basically at the same time. + if isinstance(self.session, bs.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + babase.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = curtime + else: + bs.timer(1.0, self._check_end_game) + + else: + # Default handler: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + # In co-op, we go till everyone is dead.. otherwise we go + # until one team remains. + if isinstance(self.session, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def end_game(self) -> None: + cur_time = bs.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + # Mark death-time as now for any still-living players + # and award players points for how long they lasted. + # (these per-player scores are only meaningful in team-games) + for team in self.teams: + for player in team.players: + survived = False + + # Throw an extra fudge factor in so teams that + # didn't die come out ahead of teams that did. + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + # Award a per-player score depending on how many seconds + # they lasted (per-player scores only affect teams mode; + # everywhere else just looks at the per-team score). + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 # A bit extra for survivors. + self.stats.player_scored(player, score, screenmessage=False) + + # Stop updating our time text, and set the final time to match + # exactly when our last guy died. + self._timer.stop(endtime=self._last_player_death_time) + + # Ok now calc game results: set a score for each team and then tell + # the game to end. + results = bs.GameResults() + + # Remember that 'free-for-all' mode is simply a special form + # of 'teams' mode where each player gets their own team, so we can + # just always deal in teams and have all cases covered. + for team in self.teams: + + # Set the team score to the max time survived by any player on + # that team. + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) diff --git a/plugins/minigames/bot_chase.py b/plugins/minigames/bot_chase.py new file mode 100644 index 000000000..c3b2b0704 --- /dev/null +++ b/plugins/minigames/bot_chase.py @@ -0,0 +1,218 @@ +# ba_meta require api 9 +from __future__ import annotations +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +import random +from bascenev1lib.actor.spazbot import SpazBotSet, SpazBot, SpazBotDiedMessage +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, List, Type, Optional + + +class Player(bs.Player['Team']): + """Our player type for this game""" + + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None + + +class MrSpazBot(SpazBot): + """Our bot type for this game""" + character = 'Spaz' + run = True + charge_dist_min = 10.0 + charge_dist_max = 9999.0 + charge_speed_min = 1.0 + charge_speed_max = 1.0 + throw_dist_min = 9999 + throw_dist_max = 9999 + + +class Team(bs.Team[Player]): + """Our team type for this minigame""" + + +# ba_meta export bascenev1.GameActivity +class BotChaseGame(bs.TeamGameActivity[Player, Team]): + """Our goal is to survive from spawning bots""" + name = 'Bot Chase' + description = 'Try to survive from bots!' + available_settings = [ + bs.BoolSetting( + 'Epic Mode', + default=False + ), + ] + + announce_player_deaths = True + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium'] + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + # Coop session unused + return (issubclass(sessiontype, bs.FreeForAllSession) or issubclass(sessiontype, bs.DualTeamSession) or issubclass(sessiontype, bs.CoopSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + self._bots = SpazBotSet() + self._epic_mode = bool(settings['Epic Mode']) + self._timer: Optional[OnScreenTimer] = None + self._last_player_death_time: Optional[float] = None + + if self._epic_mode: + self.slow_motion = True + self.default_music = (bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH) + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + assert self._timer is not None + player.death_time = self._timer.getstarttime() + return + self.spawn_player(player) + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + self._check_end_game() + + def spawn_player(self, player: Player) -> bs.Actor: + spaz = self.spawn_player_spaz(player) + spaz.connect_controls_to_player(enable_punch=True, + enable_bomb=True, + enable_pickup=True) + + spaz.bomb_count = 3 + spaz.bomb_type = 'normal' + + # cerdo gordo + spaz.node.color_mask_texture = bs.gettexture('melColorMask') + spaz.node.color_texture = bs.gettexture('melColor') + spaz.node.head_mesh = bs.getmesh('melHead') + spaz.node.hand_mesh = bs.getmesh('melHand') + spaz.node.torso_mesh = bs.getmesh('melTorso') + spaz.node.pelvis_mesh = bs.getmesh('kronkPelvis') + spaz.node.upper_arm_mesh = bs.getmesh('melUpperArm') + spaz.node.forearm_mesh = bs.getmesh('melForeArm') + spaz.node.upper_leg_mesh = bs.getmesh('melUpperLeg') + spaz.node.lower_leg_mesh = bs.getmesh('melLowerLeg') + spaz.node.toes_mesh = bs.getmesh('melToes') + spaz.node.style = 'mel' + # Sounds cerdo gordo + mel_sounds = [bs.getsound('mel01'), bs.getsound('mel02'), bs.getsound('mel03'), bs.getsound('mel04'), bs.getsound('mel05'), + bs.getsound('mel06'), bs.getsound('mel07'), bs.getsound('mel08'), bs.getsound('mel09'), bs.getsound('mel10')] + spaz.node.jump_sounds = mel_sounds + spaz.node.attack_sounds = mel_sounds + spaz.node.impact_sounds = mel_sounds + spaz.node.pickup_sounds = mel_sounds + spaz.node.death_sounds = [bs.getsound('melDeath01')] + spaz.node.fall_sounds = [bs.getsound('melFall01')] + + spaz.play_big_death_sound = True + return spaz + + def on_begin(self) -> None: + super().on_begin() + self._bots.spawn_bot(MrSpazBot, pos=(random.choice( + [1, -1, 2, -2]), 1.34, random.choice([1, -1, 2, -2])), spawn_time=2.0) + self._bots.spawn_bot(MrSpazBot, pos=(random.choice( + [1, -1, 2, -2]), 1.34, random.choice([1, -1, 2, -2])), spawn_time=2.0) + + self._timer = OnScreenTimer() + self._timer.start() + + bs.timer(10.0, self._spawn_this_bot, repeat=True) + bs.timer(5.0, self._check_end_game) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + super().handlemessage(msg) + + curtime = bs.time() + + msg.getplayer(Player).death_time = curtime + + if isinstance(self.session, bs.CoopSession): + babase.pushcall(self._check_end_game) + + self._last_player_death_time = curtime + else: + bs.timer(1.0, self._check_end_game) + elif isinstance(msg, SpazBotDiedMessage): + self._spawn_this_bot() + else: + return super().handlemessage(msg) + return None + + def _spawn_this_bot(self) -> None: + self._bots.spawn_bot(MrSpazBot, pos=(random.choice( + [1, -1, 2, -2]), 1.34, random.choice([1, -1, 2, -2])), spawn_time=2.0) + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + if isinstance(self.session, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def end_game(self) -> None: + cur_time = bs.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + for team in self.teams: + for player in team.players: + survived = False + + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 + self.stats.player_scored(player, score, screenmessage=False) + + self._timer.stop(endtime=self._last_player_death_time) + + results = bs.GameResults() + + for team in self.teams: + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, + player.death_time - start_time) + + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) + + +# ba_meta export babase.Plugin +class plugin(babase.Plugin): + def __init__(self): + ## Campaign support ## + babase.app.classic.add_coop_practice_level(bs.Level( + name='Bot Chase', gametype=BotChaseGame, + settings={}, + preview_texture_name='footballStadiumPreview')) diff --git a/plugins/minigames/bot_shower.py b/plugins/minigames/bot_shower.py new file mode 100644 index 000000000..bb72afc6b --- /dev/null +++ b/plugins/minigames/bot_shower.py @@ -0,0 +1,193 @@ +# ba_meta require api 9 + +from __future__ import annotations +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.actor.spazbot import ( + SpazBot, SpazBotSet, + BomberBot, BrawlerBot, BouncyBot, + ChargerBot, TriggerBot, ExplodeyBot) + +if TYPE_CHECKING: + from typing import Any, List, Type, Optional + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class BotShowerGame(bs.TeamGameActivity[Player, Team]): + """A babase.MeteorShowerGame but replaced with bots.""" + + name = 'Bot Shower' + description = 'Survive from the bots.' + available_settings = [ + bs.BoolSetting('Spaz', default=True), + bs.BoolSetting('Zoe', default=True), + bs.BoolSetting('Kronk', default=True), + bs.BoolSetting('Snake Shadow', default=True), + bs.BoolSetting('Mel', default=True), + bs.BoolSetting('Jack Morgan', default=True), + bs.BoolSetting('Easter Bunny', default=True), + bs.BoolSetting('Epic Mode', default=False), + ] + + announce_player_deaths = True + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium', 'Hockey Stadium'] + + def __init__(self, settings: dict) -> None: + super().__init__(settings) + self._epic_mode = settings['Epic Mode'] + self._last_player_death_time: Optional[float] = None + self._timer: Optional[OnScreenTimer] = None + self._bots: Optional[SpazBotSet] = None + self._bot_type: List[SpazBot] = [] + + if bool(settings['Spaz']) == True: + self._bot_type.append(BomberBot) + else: + if BomberBot in self._bot_type: + self._bot_type.remove(BomberBot) + if bool(settings['Zoe']) == True: + self._bot_type.append(TriggerBot) + else: + if TriggerBot in self._bot_type: + self._bot_type.remove(TriggerBot) + if bool(settings['Kronk']) == True: + self._bot_type.append(BrawlerBot) + else: + if BrawlerBot in self._bot_type: + self._bot_type.remove(BrawlerBot) + if bool(settings['Snake Shadow']) == True: + self._bot_type.append(ChargerBot) + else: + if ChargerBot in self._bot_type: + self._bot_type.remove(ChargerBot) + if bool(settings['Jack Morgan']) == True: + self._bot_type.append(ExplodeyBot) + else: + if ExplodeyBot in self._bot_type: + self._bot_type.remove(ExplodeyBot) + if bool(settings['Easter Bunny']) == True: + self._bot_type.append(BouncyBot) + else: + if BouncyBot in self._bot_type: + self._bot_type.remove(BouncyBot) + + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + + def on_begin(self) -> None: + super().on_begin() + self._bots = SpazBotSet() + self._timer = OnScreenTimer() + self._timer.start() + + if self._epic_mode: + bs.timer(1.0, self._start_spawning_bots) + else: + bs.timer(5.0, self._start_spawning_bots) + + bs.timer(5.0, self._check_end_game) + + def spawn_player(self, player: Player) -> None: + spaz = self.spawn_player_spaz(player) + spaz.connect_controls_to_player( + enable_punch=False, + enable_bomb=False, + enable_pickup=False) + return spaz + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + bui.screenmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(1, 1, 0), + ) + + assert self._timer is not None + player.death_time = self._timer.getstarttime() + return + self.spawn_player(player) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + curtime = bs.time() + msg.getplayer(Player).death_time = curtime + + bs.timer(1.0, self._check_end_game) + else: + super().handlemessage(msg) + + def _start_spawning_bots(self) -> None: + bs.timer(1.2, self._spawn_bot, repeat=True) + bs.timer(2.2, self._spawn_bot, repeat=True) + + def _spawn_bot(self) -> None: + assert self._bots is not None + self._bots.spawn_bot(random.choice(self._bot_type), pos=( + random.uniform(-11, 11), (9.8 if self.map.getname() == 'Football Stadium' else 5.0), random.uniform(-5, 5))) + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + if living_team_count <= 1: + self.end_game() + + def end_game(self) -> None: + cur_time = bs.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + for team in self.teams: + for player in team.players: + survived = False + + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 + self.stats.player_scored(player, score, screenmessage=False) + + self._timer.stop(endtime=self._last_player_death_time) + + results = bs.GameResults() + + for team in self.teams: + + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, + player.death_time - start_time) + + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) diff --git a/plugins/minigames/botsvsbots.py b/plugins/minigames/botsvsbots.py new file mode 100644 index 000000000..95f2115d1 --- /dev/null +++ b/plugins/minigames/botsvsbots.py @@ -0,0 +1,370 @@ +""" +Bots Fighting game. Pretty cool for if u want to bet with your mates, or let fate (randomess) decide on the winning team. + +-credits- +made by rabbitboom, aka Johnny仔 (YouTube) or iCommerade (gaming world(???)) +original idea by Froshlee08 (hence a bot named in his honour) +shoutout to EraOS who gave me quite a lot of solution and Brotherboard who also helped. +""" + +# ba_meta require api 9 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING, override + + +import logging +import random +import weakref +import bascenev1 as bs +import efro.debug as Edebug + +from bascenev1lib.actor.spaz import Spaz, PunchHitMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.spazbot import ( + SpazBotDiedMessage, + SpazBotSet, + ChargerBot, + StickyBot, + SpazBot, + BrawlerBot, + TriggerBot, + BrawlerBotProShielded, + ChargerBotProShielded, + BouncyBot, + TriggerBotProShielded, + BomberBotProShielded, +) + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class SpazBot2(SpazBot): + # custom bots that have custom behaviours requiring more msg and modules from spaz.py will have them imported here. + from bascenev1lib.actor.spaz import Spaz, PunchHitMessage + + +class BouncyBotSemiLite(BouncyBot): + # nerfed bouncybot. this is due to the base bouncybot being the same lv as pro bots. + highlight = (1, 1, 0.8) + punchiness = 0.85 + run = False + default_boxing_gloves = False + charge_dist_min = 5 + points_mult = 1 + + +class FroshBot(SpazBot): + # Slightly beefier version of BomberBots, with slightly altered behaviour. + color = (0.13, 0.13, 0.13) + highlight = (0.2, 1, 1) + character = 'Bernard' + run = True + default_bomb_count = 2 + throw_rate = 0.8 + throwiness = 0.2 + punchiness = 0.85 + charge_dist_max = 3.0 + charge_speed_min = 0.3 + default_hitpoints = 1300 + + +class FroshBotShielded(FroshBot): + default_shields = True + + +class StickyBotShielded(StickyBot): + # shielded stickybots. *not bonus bots cuz they act the same as normal stickybots + default_shields = True + + +class IcePuncherBot(SpazBot2): + # A bot who can freeze anyone on punch and is immune to freezing. + color = (0, 0, 1) + highlight = (.2, .2, 1) + character = 'Pascal' + run = True + punchiness = 0.8 + charge_dist_min = 6 + charge_dist_max = 9999 + charge_speed_min = 0.3 + charge_speed_max = 0.85 + throw_dist_min = 9999 + throw_dist_max = 9999 + + def handlemessage(self, msg): + # Add the ability to freeze anyone on punch. + if isinstance(msg, self.PunchHitMessage): + node = bs.getcollision().opposingnode + try: + node.handlemessage(bs.FreezeMessage()) + bs.getsound('freeze').play() + except Exception: + print('freeze failed.') + return super().handlemessage(msg) + elif isinstance(msg, bs.FreezeMessage): + pass # resistant to freezing. + else: + return super().handlemessage(msg) + + +class IcePuncherBotShielded(IcePuncherBot): + default_shields = True + + +class GliderBot(BrawlerBot): + # A bot who can glide on terrain and navigate it safely as if it's ice skating. + color = (0.5, 0.5, 0.5) + highlight = (0, 10, 0) + character = 'B-9000' + charge_speed_min = 0.3 + charge_speed_max = 1.0 + + def __init__(self) -> None: + super().__init__() + self.node.hockey = True # Added the ability to move fast "like in hockey". + + +class GliderBotShielded(GliderBot): + # Shielded version of GliderBot. + default_shields = True + + +class TeamBotSet(SpazBotSet): + """Bots that can gather in a team and kill other bots.""" + + activity: BotsVSBotsGame + + def __init__(self, team: Team) -> None: + super().__init__() + activity = bs.getactivity() + self._pro_bots = activity._pro_bots + self._bonus_bots = activity._bonus_bots + self._punching_bots_only = activity._punching_bots_only + self.spawn_time = activity.spawn_time + self._team = weakref.ref(team) + + def colorforteam(self, spaz) -> None: + spaz.node.color = self.team.color + + def yell(self) -> None: + for botlist in self._bot_lists: + for bot in botlist: + if bot: + assert bot.node + self.celebrate(self.spawn_time) + bs.Call(bot.node.handlemessage, 'jump_sound') + bs.timer(0, bs.Call(bot.node.handlemessage, 'attack_sound')) + + def _update(self) -> None: + # Update one of our bot lists each time through. + # First off, remove no-longer-existing bots from the list. + try: + bot_list = self._bot_lists[self._bot_update_list] = [ + b for b in self._bot_lists[self._bot_update_list] if b + ] + except Exception: + bot_list = [] + logging.exception( + 'Error updating bot list: %s', + self._bot_lists[self._bot_update_list], + ) + self._bot_update_list = ( + self._bot_update_list + 1 + ) % self._bot_list_count + + # Update our list of player points for the bots to use. + player_pts = [] + try: + for n in bs.getnodes(): + if n.getnodetype() == 'spaz': + s = n.getdelegate(object) + if isinstance(s, SpazBot): + if not s in self.get_living_bots(): + if s.is_alive(): + player_pts.append(( + bs.Vec3(n.position), + bs.Vec3(n.velocity))) + except Exception: + logging.exception('Error on bot-set _update.') + + for bot in bot_list: + bot.set_player_points(player_pts) + bot.update_ai() + + def get_bot_type(self): + bot_types = [ + BrawlerBot, + ChargerBot, + BouncyBotSemiLite, + ] + if self._punching_bots_only == False: + bot_types += [ + SpazBot, + StickyBot, + ] + if self._pro_bots == True: + bot_types += [ + BrawlerBotProShielded, + ChargerBotProShielded, + BouncyBot, + ] + if self._punching_bots_only == False: + bot_types += [ + BomberBotProShielded, + StickyBotShielded, + ] + if self._bonus_bots == True: + bot_types += [ + IcePuncherBot, + GliderBot, + ] + if self._punching_bots_only == False: + bot_types += [ + FroshBot, + ] + if self._bonus_bots and self._pro_bots == True: + bot_types += [ + IcePuncherBotShielded, + GliderBotShielded, + ] + if self._punching_bots_only == False: + bot_types += [ + FroshBotShielded, + ] + return random.choice(bot_types) + + @property + def team(self) -> Team: + """The bot's team.""" + return self._team() + + +class Team(bs.Team[TeamBotSet]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + self.bots = TeamBotSet(self) + + +# ba_meta export bascenev1.GameActivity +class BotsVSBotsGame(bs.TeamGameActivity[bs.Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Bots VS Bots' + description = 'Sit down and enjoy mobs from your team fight the opposing team for you. \n Feel free to enjoy some popcorn with it or place a bet with your friends.' + + @override + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[bs.Setting]: + settings = [ + bs.IntSetting( + 'Bots Per Team', + min_value=5, + default=20, + increment=5, + ), + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Punchers only', default=False), + bs.BoolSetting('Pro bots', default=False), + bs.BoolSetting('Bonus bots', default=True), + ] + return settings + + @override + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @override + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._bots_per_team = int(settings['Bots Per Team']) + self._dingsound = bs.getsound('dingSmall') + self._punching_bots_only = bool(settings['Punchers only']) + self._pro_bots = bool(settings['Pro bots']) + self._bonus_bots = bool(settings['Bonus bots']) + self._epic_mode = bool(settings['Epic Mode']) + self._cheersound = bs.getsound('cheer') + self._marchsoundA = bs.getsound('footimpact01') + self._marchsoundB = bs.getsound('footimpact02') + self._marchsoundC = bs.getsound('footimpact03') + + # Base class overrides. + self.slow_motion = self._epic_mode + self.spawn_time = 1.6 if self._epic_mode else 4.0 + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.TO_THE_DEATH) + + @override + def get_instance_description(self) -> str | Sequence: + return 'Enjoy the battle.' + + @override + def get_instance_description_short(self) -> str | Sequence: + return 'Enjoy the battle.' + + @override + def on_team_join(self, team: Team) -> None: + self.spawn_team(team) + + @override + def on_begin(self) -> None: + bs.TeamGameActivity.on_begin(self) + + def spawn_player(self, player: Player) -> None: + return None + + def spawn_team(self, team: Team) -> None: + camp = self.map.get_flag_position(team.id) + for i in range(self._bots_per_team): + team.bots.spawn_bot(team.bots.get_bot_type(), + (camp[0], camp[1] + 1.8, random.randrange(-4, 5)), + self.spawn_time, team.bots.colorforteam) + bs.timer(float(self.spawn_time) + 1.7, team.bots.yell) + + def update(self) -> None: + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0, self.end_game) + + def _get_living_teams(self) -> list[Team]: + return [ + team + for team in self.teams + if team.bots.have_living_bots() + ] + + @override + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, SpazBotDiedMessage): + bs.pushcall(self.update) + + else: + return super().handlemessage(msg) + return None + + @override + def end_game(self) -> None: + if self.has_ended(): + return + results = bs.GameResults() + self._cheersound.play() + for team in self.teams: + team.bots.final_celebrate() + results.set_team_score(team, len(team.bots.get_living_bots())) + self.end(results=results) diff --git a/plugins/minigames/boxing.py b/plugins/minigames/boxing.py new file mode 100644 index 000000000..b0a1af2fd --- /dev/null +++ b/plugins/minigames/boxing.py @@ -0,0 +1,239 @@ +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.game.deathmatch import DeathMatchGame + +if TYPE_CHECKING: + from typing import Sequence + + +lang = bs.app.lang.language + +if lang == 'Spanish': + name = 'Super Boxeo' + description = ('¡Sin bombas!\n' + '¡Noquea a los enemigos con tus propias manos!\n') + super_jump_text = 'Super Salto' + enable_powerups = 'Habilitar Potenciadores' +else: + name = 'Super Boxing' + description = ('No bombs!\n' + 'Knock out your enemies using your bare hands!\n') + super_jump_text = 'Super Jump' + enable_powerups = 'Enable Powerups' + + +class NewPlayerSpaz(PlayerSpaz): + + def __init__(self, + player: bs.Player, + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + character: str = 'Spaz', + powerups_expire: bool = True, + super_jump: bool = False): + super().__init__(player=player, + color=color, + highlight=highlight, + character=character, + powerups_expire=powerups_expire) + from bascenev1lib.gameutils import SharedObjects + shared = SharedObjects.get() + self._super_jump = super_jump + self.jump_mode = False + self.super_jump_material = bs.Material() + self.super_jump_material.add_actions( + conditions=('they_have_material', shared.footing_material), + actions=( + ('call', 'at_connect', babase.Call(self.jump_state, True)), + ('call', 'at_disconnect', babase.Call(self.jump_state, False)) + ), + ) + self.node.roller_materials += (self.super_jump_material, ) + + def jump_state(self, mode: bool) -> None: + self.jump_mode = mode + + def on_jump_press(self) -> None: + """ + Called to 'press jump' on this spaz; + used by player or AI connections. + """ + if not self.node: + return + t_ms = int(bs.time() * 1000.0) + assert isinstance(t_ms, int) + if t_ms - self.last_jump_time_ms >= self._jump_cooldown: + self.node.jump_pressed = True + self.last_jump_time_ms = t_ms + if self._player.is_alive() and self.jump_mode and ( + self._super_jump): + def do_jump(): + self.node.handlemessage( + 'impulse', + self.node.position[0], + self.node.position[1], + self.node.position[2], + 0, 0, 0, 95, 95, 0, 0, 0, 1, 0 + ) + bs.timer(0.0, do_jump) + bs.timer(0.1, do_jump) + bs.timer(0.2, do_jump) + self._turbo_filter_add_press('jump') + + +# ba_meta export bascenev1.GameActivity +class BoxingGame(DeathMatchGame): + + name = name + description = description + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting(super_jump_text, default=False), + bs.BoolSetting(enable_powerups, default=False), + bs.BoolSetting('Epic Mode', default=False), + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', default=False) + ) + + return settings + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: int | None = None + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int(settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False) + ) + self._super_jump = bool(settings[super_jump_text]) + self._enable_powerups = bool(settings[enable_powerups]) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.TO_THE_DEATH + ) + + def on_begin(self) -> None: + bs.TeamGameActivity.on_begin(self) + self.setup_standard_time_limit(self._time_limit) + if self._enable_powerups: + self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = self._kills_to_win_per_player * max( + 1, max(len(t.players) for t in self.teams) + ) + self._update_scoreboard() + + def _standard_drop_powerup(self, index: int, expire: bool = True) -> None: + # pylint: disable=cyclic-import + from bascenev1lib.actor.powerupbox import PowerupBox, PowerupBoxFactory + + PowerupBox( + position=self.map.powerup_spawn_points[index], + poweruptype=PowerupBoxFactory.get().get_random_powerup_type( + excludetypes=['triple_bombs', 'ice_bombs', 'impact_bombs', + 'land_mines', 'sticky_bombs', 'punch'] + ), + expire=expire, + ).autoretain() + + def spawn_player(self, player: Player) -> bs.Actor: + import random + from babase import _math + from bascenev1._gameutils import animate + + if isinstance(self.session, bs.DualTeamSession): + position = self.map.get_start_position(player.team.id) + else: + # otherwise do free-for-all spawn locations + position = self.map.get_ffa_start_position(self.players) + angle = None + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = babase.safecolor(color, target_intensity=0.75) + + spaz = NewPlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player, + super_jump=self._super_jump) + + player.actor = spaz + assert spaz.node + + spaz.node.name = name + spaz.node.name_color = display_color + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + # custom + spaz.connect_controls_to_player(enable_bomb=False) + spaz.equip_boxing_gloves() + + return spaz diff --git a/plugins/minigames/canon_fight.py b/plugins/minigames/canon_fight.py new file mode 100644 index 000000000..636a32209 --- /dev/null +++ b/plugins/minigames/canon_fight.py @@ -0,0 +1,445 @@ +# Released under the MIT License. See LICENSE for details. +# Created by Mr.Smoothy - +# https://discord.gg/ucyaesh +# https://bombsquad-community.web.app/home for more mods. +# +"""DeathMatch game and support classes.""" + +# ba_meta require api 9 +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +import random +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.bomb import BombFactory +from bascenev1lib.actor.bomb import Bomb + +from bascenev1lib.game.deathmatch import DeathMatchGame, Player, Team + +if TYPE_CHECKING: + from typing import Any, Union, Sequence, Optional + + +# ba_meta export bascenev1.GameActivity +class CanonFightGame(DeathMatchGame): + + """A game type based on acquiring kills.""" + + name = 'Canon Fight' + description = 'Kill a set number of enemies to win.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ["Step Right Up"] + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + self.wtindex = 0 + self.wttimer = bs.timer(5, babase.Call(self.wt_), repeat=True) + self.wthighlights = ["Created by Mr.Smoothy", + "hey smoothy youtube", "smoothy#multiverse"] + + def wt_(self): + node = bs.newnode('text', + attrs={ + 'text': self.wthighlights[self.wtindex], + 'flatness': 1.0, + 'h_align': 'center', + 'v_attach': 'bottom', + 'scale': 0.7, + 'position': (0, 20), + 'color': (0.5, 0.5, 0.5) + }) + + self.delt = bs.timer(4, node.delete) + self.wtindex = int((self.wtindex+1) % len(self.wthighlights)) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Crush ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + self.create_canon_A() + self.create_canon_B() + self.create_wall() + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + self._dingsound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + self._dingsound.play() + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + self.delete_text_nodes() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def delete_text_nodes(self): + self.canon.delete() + self.canon_.delete() + self.canon2.delete() + self.canon_2.delete() + self.curve.delete() + self.curve2.delete() + + def _handle_canon_load_A(self): + try: + bomb = bs.getcollision().opposingnode.getdelegate(Bomb, True) + # pos=bomb.position + owner = bomb.owner + type = bomb.bomb_type + source_player = bomb.get_source_player(bs.Player) + bs.getcollision().opposingnode.delete() + + # bomb.delete() + self.launch_bomb_byA(owner, type, source_player, 2) + except bs.NotFoundError: + # This can happen if the flag stops touching us due to being + # deleted; that's ok. + return + + def _handle_canon_load_B(self): + try: + bomb = bs.getcollision().opposingnode.getdelegate(Bomb, True) + # pos=bomb.position + owner = bomb.owner + type = bomb.bomb_type + source_player = bomb.get_source_player(bs.Player) + bs.getcollision().opposingnode.delete() + + # bomb.delete() + self.launch_bomb_byB(owner, type, source_player, 2) + except bs.NotFoundError: + # This can happen if the flag stops touching us due to being + # deleted; that's ok. + return + + def launch_bomb_byA(self, owner, type, source_player, count): + if count > 0: + y = random.randrange(2, 9, 2) + z = random.randrange(-4, 6) + self.fake_explosion( + (-5.708631629943848, 7.437141418457031, -4.525400638580322)) + + Bomb(position=(-6, 7.5, -4), bomb_type=type, owner=owner, + source_player=source_player, velocity=(19, y, z)).autoretain() + bs.timer(0.6, babase.Call(self.launch_bomb_byA, + owner, type, source_player, count-1)) + else: + return + + def launch_bomb_byB(self, owner, type, source_player, count): + if count > 0: + y = random.randrange(2, 9, 2) + z = random.randrange(-4, 6) + self.fake_explosion( + (5.708631629943848, 7.437141418457031, -4.525400638580322)) + + Bomb(position=(6, 7.5, -4), bomb_type=type, owner=owner, + source_player=source_player, velocity=(-19, y, z)).autoretain() + bs.timer(0.6, babase.Call(self.launch_bomb_byB, + owner, type, source_player, count-1)) + else: + return + + def fake_explosion(self, position: Sequence[float]): + explosion = bs.newnode('explosion', + attrs={'position': position, + 'radius': 1, 'big': False}) + bs.timer(0.4, explosion.delete) + sounds = ['explosion0'+str(n) for n in range(1, 6)] + sound = random.choice(sounds) + bs.getsound(sound).play() + + def create_canon_A(self): + shared = SharedObjects.get() + canon_load_mat = bs.Material() + factory = BombFactory.get() + + canon_load_mat.add_actions( + + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + + )) + canon_load_mat.add_actions( + conditions=('they_have_material', factory.bomb_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True), + ('call', 'at_connect', babase.Call(self._handle_canon_load_A)) + ), + ) + self.ud_1_r = bs.newnode('region', attrs={'position': (-8.908631629943848, 7.337141418457031, - + 4.525400638580322), 'scale': (2, 1, 1), 'type': 'box', 'materials': [canon_load_mat]}) + + self.node = bs.newnode('shield', + delegate=self, + attrs={ + 'position': (-8.308631629943848, 7.337141418457031, -4.525400638580322), + 'color': (0.3, 0.2, 2.8), + 'radius': 1.3 + }) + self.canon = bs.newnode('text', + attrs={ + 'text': '___________', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.3, 0.3, 0.8), + 'scale': 0.019, + 'h_align': 'left', + 'position': (-8.388631629943848, 7.837141418457031, -4.525400638580322) + }) + self.canon_ = bs.newnode('text', + attrs={ + 'text': '_________', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.3, 0.3, 0.8), + 'scale': 0.019, + 'h_align': 'left', + 'position': (-7.888631629943848, 7.237141418457031, -4.525400638580322) + }) + self.curve = bs.newnode('text', + attrs={ + 'text': '/\n', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.3, 0.3, 0.8), + 'scale': 0.019, + 'h_align': 'left', + 'position': (-8.788631629943848, 7.237141418457031, -4.525400638580322) + }) + + def create_canon_B(self): + shared = SharedObjects.get() + canon_load_mat = bs.Material() + factory = BombFactory.get() + + canon_load_mat.add_actions( + + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + + )) + canon_load_mat.add_actions( + conditions=('they_have_material', factory.bomb_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True), + ('call', 'at_connect', babase.Call(self._handle_canon_load_B)) + ), + ) + self.ud_1_r2 = bs.newnode('region', attrs={'position': ( + 8.908631629943848+0.81, 7.327141418457031, -4.525400638580322), 'scale': (2, 1, 1), 'type': 'box', 'materials': [canon_load_mat]}) + + self.node2 = bs.newnode('shield', + delegate=self, + attrs={ + 'position': (8.308631629943848+0.81, 7.327141418457031, -4.525400638580322), + 'color': (2.3, 0.2, 0.3), + 'radius': 1.3 + }) + self.canon2 = bs.newnode('text', + attrs={ + 'text': '___________', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.8, 0.3, 0.3), + 'scale': 0.019, + 'h_align': 'right', + 'position': (8.388631629943848+0.81, 7.837141418457031, -4.525400638580322) + }) + self.canon_2 = bs.newnode('text', + attrs={ + 'text': '_________', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.8, 0.3, 0.3), + 'scale': 0.019, + 'h_align': 'right', + 'position': (7.888631629943848+0.81, 7.237141418457031, -4.525400638580322) + }) + self.curve2 = bs.newnode('text', + attrs={ + 'text': '\\', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (0.8, 0.3, 0.3), + 'scale': 0.019, + 'h_align': 'right', + 'position': (8.788631629943848+0.81, 7.237141418457031, -4.525400638580322) + }) + + def create_wall(self): + shared = SharedObjects.get() + factory = BombFactory.get() + mat = bs.Material() + mat.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + )) + mat.add_actions( + conditions=( + ('they_have_material', factory.bomb_material)), + actions=( + ('modify_part_collision', 'collide', False) + )) + self.wall = bs.newnode('region', attrs={'position': ( + 0.36877517104148865, 4.312626838684082, -8.68477725982666), 'scale': (3, 7, 27), 'type': 'box', 'materials': [mat]}) diff --git a/plugins/minigames/castel_queen.py b/plugins/minigames/castel_queen.py new file mode 100644 index 000000000..f90b32dac --- /dev/null +++ b/plugins/minigames/castel_queen.py @@ -0,0 +1,409 @@ +# Released under the MIT License. See LICENSE for details. +# +""" +CastelQueen - Carry the Queen alone or with your team. +Author: Mr.Smoothy +Discord: https://discord.gg/ucyaesh +Youtube: https://www.youtube.com/c/HeySmoothy +Website: https://bombsquad-community.web.app +Github: https://github.com/bombsquad-community +""" +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs + +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.game.keepaway import KeepAwayGame, FlagState, Player +from bascenev1lib.actor import spaz +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List + +# ba_meta export bascenev1.GameActivity + + +class ChooseQueen(KeepAwayGame): + name = 'FCUK The Queen' + description = 'Carry the queen for a set length of time' + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Creative Thoughts'] + + def get_instance_description(self) -> str | Sequence: + return 'FCUK the queen for ${ARG1} seconds.', self._hold_time + + def get_instance_description_short(self) -> str | Sequence: + return 'FCUK the queen for ${ARG1} seconds', self._hold_time + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self.lifts = {} + self._room_wall_material = bs.Material() + self._room_wall_material.add_actions( + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + )) + self._queen_material = bs.Material() + self._queen_material.add_actions( + conditions=('they_have_material', self._room_wall_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + )) + self._queen_material.add_actions( + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + )) + self._room_wall_material.add_actions( + conditions=('they_have_material', self._queen_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + )) + self._real_wall_material = bs.Material() + self._real_wall_material.add_actions( + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + )) + + self._real_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + + def on_begin(self): + bs.getactivity().globalsnode.happy_thoughts_mode = True + super().on_begin() + self.make_map() + + def _spawn_flag(self) -> None: + self._swipsound.play() + self._flash_flag_spawn() + assert self._flag_spawn_pos is not None + shared = SharedObjects.get() + self._flag = spaz.Spaz(color=(0, 0, 0), character="Pixel").autoretain() + self._flag.handlemessage(bs.StandMessage((0, 14.63, -5.52), 93)) + self._flag.node.hold_position_pressed = True + self._flag.node.materials = (self._queen_material, shared.object_material) + # self._flag.node.extras_material= tuple(list(self._flag.node.extras_material).append(self._queen_materia)) + self._flag.hitpoints = 5000 + self._flag.hitpoints_max = 5000 + + self._flag_state = FlagState.NEW + self._flag_light = bs.newnode( + 'light', + owner=self._flag.node, + attrs={'intensity': 0.2, 'radius': 0.3, 'color': (0.2, 0.2, 0.2)}, + ) + assert self._flag.node + self._flag.node.connectattr('position', self._flag_light, 'position') + self._update_flag_state() + + def _update_flag_state(self) -> None: + if not self._flag.node.exists(): + self._spawn_flag() + for team in self.teams: + team.holdingflag = False + self._holding_players = [] + for player in self.players: + holdingflag = False + try: + assert isinstance(player.actor, (PlayerSpaz, type(None))) + if ( + player.actor + and player.actor.node + and player.actor.node.hold_node + ): + holdingflag = ( + player.actor.node.hold_node == self._flag.node + ) + except Exception: + babase.print_exception('Error checking hold flag.') + if holdingflag: + self._holding_players.append(player) + player.team.holdingflag = True + + holdingteams = set(t for t in self.teams if t.holdingflag) + prevstate = self._flag_state + assert self._flag is not None + assert self._flag_light + assert self._flag.node + if len(holdingteams) > 1: + self._flag_state = FlagState.CONTESTED + self._scoring_team = None + elif len(holdingteams) == 1: + holdingteam = list(holdingteams)[0] + self._flag_state = FlagState.HELD + self._scoring_team = holdingteam + else: + self._flag_state = FlagState.UNCONTESTED + self._scoring_team = None + + if self._flag_state != prevstate: + self._swipsound.play() + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + elif isinstance(msg, (bs.PickedUpMessage, bs.DroppedMessage)): + self._update_flag_state() + else: + super().handlemessage(msg) + + def make_map(self): + shared = SharedObjects.get() + bs.get_foreground_host_activity()._map.leftwall.materials = [ + shared.footing_material, self._real_wall_material] + + bs.get_foreground_host_activity()._map.rightwall.materials = [ + shared.footing_material, self._real_wall_material] + + bs.get_foreground_host_activity()._map.topwall.materials = [ + shared.footing_material, self._real_wall_material] + + self.floorwall1 = bs.newnode('region', attrs={'position': (-10, 5, -5.52), 'scale': + (15, 0.2, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.floorwall2 = bs.newnode('region', attrs={'position': (10, 5, -5.52), 'scale': ( + 15, 0.2, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + + self.wall1 = bs.newnode('region', attrs={'position': (0, 11, -6.90), 'scale': ( + 35.4, 20, 1), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.wall2 = bs.newnode('region', attrs={'position': (0, 11, -4.14), 'scale': ( + 35.4, 20, 1), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + + bs.newnode('locator', attrs={'shape': 'box', 'position': (-10, 5, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (15, 0.2, 2)}) + + bs.newnode('locator', attrs={'shape': 'box', 'position': (10, 5, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (15, 0.2, 2)}) + + self.create_static_step(0, 14.29) + # upper right + self.create_static_step(11, 16) + self.create_slope(8, 16, False) + self.create_static_step(3, 18) + + # lower right + self.create_static_step(11, 12) + self.create_slope(6, 10, True) + self.create_static_step(3, 10) + + # upper left + self.create_static_step(-11, 16) + self.create_slope(-8, 16, True) + self.create_static_step(-3, 18) + + # lower left + self.create_static_step(-11, 12) + self.create_slope(-6, 10, False) + self.create_static_step(-3, 10) + + # create queen personal room + self.room_wall_left = bs.newnode('region', attrs={'position': (-3.633, 16.63, -5.52), 'scale': + (2, 4, 5), 'type': 'box', 'materials': [shared.footing_material, self._room_wall_material]}) + self.room_wall_right = bs.newnode('region', attrs={'position': (3.533, 16.63, -5.52), 'scale': + (2, 4, 5), 'type': 'box', 'materials': [shared.footing_material, self._room_wall_material]}) + + def create_static_step(self, x, y): + shared = SharedObjects.get() + bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': (5.5, 0.1, 6), + 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (x, y, -5.52), 'color': ( + 1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (5.5, 0.1, 2)}) + + def create_slope(self, x, y, backslash): + shared = SharedObjects.get() + + for _ in range(0, 21): + bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': (0.2, 0.1, 6), + 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (x, y, -5.52), 'color': ( + 1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.2, 0.1, 2)}) + if backslash: + x = x + 0.1 + y = y + 0.1 + else: + x = x - 0.1 + y = y + 0.1 + + +class mapdefs: + points = {} + # noinspection PyDictCreation + boxes = {} + boxes['area_of_interest_bounds'] = (-1.045859963, 12.67722855, + -5.401537075) + (0.0, 0.0, 0.0) + ( + 42.46156851, 20.94044653, 0.6931564611) + points['ffa_spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) + points['ffa_spawn2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) + points['ffa_spawn3'] = (9.55724115, 11.30789446, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn4'] = (-11.55747023, 10.99170684, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn5'] = (-1.878892369, 9.46490571, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn6'] = (-0.4912812943, 5.077006397, -5.521672101) + ( + 1.878332089, 1.453808816, 0.007578097856) + points['flag1'] = (-11.75152479, 8.057427485, -5.52) + points['flag2'] = (9.840909039, 8.188634282, -5.52) + points['flag3'] = (-0.2195258696, 5.010273907, -5.52) + points['flag4'] = (-0.04605809154, 12.73369108, -5.52) + points['flag_default'] = (-0.04201942896, 12.72374492, -5.52) + boxes['map_bounds'] = (-0.8748348681, 9.212941713, -5.729538885) + ( + 0.0, 0.0, 0.0) + (42.09666006, 26.19950145, 7.89541168) + points['powerup_spawn1'] = (1.160232442, 6.745963662, -5.469115985) + points['powerup_spawn2'] = (-1.899700206, 10.56447241, -5.505721177) + points['powerup_spawn3'] = (10.56098871, 12.25165669, -5.576232453) + points['powerup_spawn4'] = (-12.33530337, 12.25165669, -5.576232453) + points['spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) + points['spawn2'] = (7.484707127, 8.172681752, + -5.614479365) + (1.553861796, 1.453808816, 0.04419853907) + points['spawn_by_flag1'] = (-9.295167711, 8.010664315, -5.44451005) + ( + 1.555840357, 1.453808816, 0.1165648888) + points['spawn_by_flag2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) + points['spawn_by_flag3'] = (-1.45994593, 5.038762459, -5.535288724) + ( + 0.9516389866, 0.6666414677, 0.08607244075) + points['spawn_by_flag4'] = (0.4932087091, 12.74493212, -5.598987003) + ( + 0.5245740665, 0.5245740665, 0.01941146064) + + +class CreativeThoughts(bs.Map): + """Freaking map by smoothy.""" + + defs = mapdefs + + name = 'Creative Thoughts' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [ + 'melee', 'keep_away', 'team_flag' + ] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'alwaysLandPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'mesh': bs.getmesh('alwaysLandLevel'), + 'bottom_mesh': bs.getmesh('alwaysLandLevelBottom'), + 'bgmesh': bs.getmesh('alwaysLandBG'), + 'collision_mesh': bs.getcollisionmesh('alwaysLandLevelCollide'), + 'tex': bs.gettexture('alwaysLandLevelColor'), + 'bgtex': bs.gettexture('alwaysLandBGColor'), + 'vr_fill_mound_mesh': bs.getmesh('alwaysLandVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound') + } + return data + + @classmethod + def get_music_type(cls) -> bs.MusicType: + return bs.MusicType.FLYING + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, -3.7, 2.5)) + shared = SharedObjects.get() + self._fake_wall_material = bs.Material() + self._real_wall_material = bs.Material() + self._fake_wall_material.add_actions( + conditions=(('they_are_younger_than', 9000), 'and', + ('they_have_material', shared.player_material)), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._real_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + 'background': True, + 'color_texture': bs.gettexture("rampageBGColor") + }) + + self.leftwall = bs.newnode('region', attrs={'position': (-17.75152479, 13, -5.52), 'scale': ( + 0.1, 15.5, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.rightwall = bs.newnode('region', attrs={'position': (17.75, 13, -5.52), 'scale': ( + 0.1, 15.5, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.topwall = bs.newnode('region', attrs={'position': (0, 21.0, -5.52), 'scale': ( + 35.4, 0.2, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (-17.75152479, 13, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.1, 15.5, 2)}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (17.75, 13, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.1, 15.5, 2)}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (0, 21.0, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (35.4, 0.2, 2)}) + + gnode = bs.getactivity().globalsnode + gnode.happy_thoughts_mode = True + gnode.shadow_offset = (0.0, 8.0, 5.0) + gnode.tint = (1.3, 1.23, 1.0) + gnode.ambient_color = (1.3, 1.23, 1.0) + gnode.vignette_outer = (0.64, 0.59, 0.69) + gnode.vignette_inner = (0.95, 0.95, 0.93) + gnode.vr_near_clip = 1.0 + self.is_flying = True + + # throw out some tips on flying + txt = bs.newnode('text', + attrs={ + 'text': babase.Lstr(resource='pressJumpToFlyText'), + 'scale': 1.2, + 'maxwidth': 800, + 'position': (0, 200), + 'shadow': 0.5, + 'flatness': 0.5, + 'h_align': 'center', + 'v_attach': 'bottom' + }) + cmb = bs.newnode('combine', + owner=txt, + attrs={ + 'size': 4, + 'input0': 0.3, + 'input1': 0.9, + 'input2': 0.0 + }) + bs.animate(cmb, 'input3', {3.0: 0, 4.0: 1, 9.0: 1, 10.0: 0}) + cmb.connectattr('output', txt, 'color') + bs.timer(10.0, txt.delete) + + +try: + bs._map.register_map(CreativeThoughts) +except: + pass diff --git a/plugins/minigames/collector.py b/plugins/minigames/collector.py new file mode 100644 index 000000000..066001094 --- /dev/null +++ b/plugins/minigames/collector.py @@ -0,0 +1,634 @@ +# ba_meta require api 9 + +''' + Gamemode: Collector + Creator: TheMikirog + Website: https://bombsquadjoyride.blogspot.com/ + + This is a gamemode purely made by me just to spite unchallenged modders + out there that put out crap to the market. + We don't want gamemodes that are just the existing ones + with some novelties! Gamers deserve more! + + In this gamemode you have to kill others in order to get their Capsules. + Capsules can be collected and staked in your inventory, + how many as you please. + After you kill an enemy that carries some of them, + they drop a respective amount of Capsules they carried + two more. + Your task is to collect these Capsules, + get to the flag and score them KOTH style. + You can't score if you don't have any Capsules with you. + The first player or team to get to the required ammount wins. + This is a gamemode all about trying to stay alive + and picking your battles in order to win. + A rare skill in BombSquad, where everyone is overly aggressive. +''' + +from __future__ import annotations + +import weakref +from enum import Enum +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +import random +from bascenev1lib.actor.flag import Flag +from bascenev1lib.actor.popuptext import PopupText +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = bs.app.lang.language +if lang == 'Spanish': + name = 'Coleccionista' + description = ('Elimina a tus oponentes para robar sus cápsulas.\n' + '¡Recolecta y anota en el punto de depósito!') + description_ingame = 'Obtén ${ARG1} cápsulas de tus enemigos.' + description_short = 'colecciona ${ARG1} cápsulas' + tips = [( + '¡Si tu oponente cae fuera del mapa, sus cápsulas desapareceran!\n' + 'No intestes matar a tus enemigos arrojándolos al vacio.'), + 'No te apresures. ¡Puedes perder tus cápsulas rápidamente!', + ('¡No dejes que el jugador con más cápsulas anote!\n' + '¡Intenta atraparlo si puedes!'), + ('¡Las Capsulas de la Suerte te dan 4 cápsulas en lugar de 2' + 'y tienen un 8% de probabilidad de aparecer después de matar'), + ('¡No te quedes en un solo lugar! Muevete más rapido que tu enemigo, ' + '¡con suerte conseguirás algunas cápsulas!'), + ] + capsules_to_win = 'Cápsulas para Ganar' + capsules_death = 'Cápsulas al Morir' + lucky_capsules = 'Cápsulas de la Suerte' + bonus = '¡BONUS!' + full_capacity = '¡Capacidad Completa!' +else: + name = 'Collector' + description = ('Kill your opponents to steal their Capsules.\n' + 'Collect them and score at the Deposit point!') + description_ingame = 'Score ${ARG1} capsules from your enemies.' + description_short = 'collect ${ARG1} capsules' + tips = [( + 'Making you opponent fall down the pit makes his Capsules wasted!\n' + 'Try not to kill enemies by throwing them off the cliff.'), + 'Don\'t be too reckless. You can lose your loot quite quickly!', + ('Don\'t let the leading player score his Capsules ' + 'at the Deposit Point!\nTry to catch him if you can!'), + ('Lucky Capsules give 4 to your inventory and they have 8% chance ' + 'of spawning after kill!'), + ('Don\'t camp in one place! Make your move first, ' + 'so hopefully you get some dough!'), + ] + capsules_to_win = 'Capsules to Win' + capsules_death = 'Capsules on Death' + lucky_capsules = 'Allow Lucky Capsules' + bonus = 'BONUS!' + full_capacity = 'Full Capacity!' + + +class FlagState(Enum): + """States our single flag can be in.""" + + NEW = 0 + UNCONTESTED = 1 + CONTESTED = 2 + HELD = 3 + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.time_at_flag = 0 + self.capsules = 0 + self.light = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class CollectorGame(bs.TeamGameActivity[Player, Team]): + + name = name + description = description + tips = tips + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + capsules_to_win, + min_value=1, + default=10, + increment=1, + ), + bs.IntSetting( + capsules_death, + min_value=1, + max_value=10, + default=2, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting(lucky_capsules, default=True), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps('keep_away') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._score_to_win: int | None = None + self._swipsound = bs.getsound('swip') + self._lucky_sound = bs.getsound('ding') + + self._flag_pos: Sequence[float] | None = None + self._flag_state: FlagState | None = None + self._flag: Flag | None = None + self._flag_light: bs.Node | None = None + self._scoring_team: weakref.ref[Team] | None = None + self._time_limit = float(settings['Time Limit']) + self._epic_mode = bool(settings['Epic Mode']) + + self._capsules_to_win = int(settings[capsules_to_win]) + self._capsules_death = int(settings[capsules_death]) + self._lucky_capsules = bool(settings[lucky_capsules]) + self._capsules: list[Any] = [] + + self._capsule_mesh = bs.getmesh('bomb') + self._capsule_tex = bs.gettexture('bombColor') + self._capsule_lucky_tex = bs.gettexture('bombStickyColor') + self._collect_sound = bs.getsound('powerup01') + self._lucky_collect_sound = bs.getsound('cashRegister2') + + self._capsule_material = bs.Material() + self._capsule_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=('call', 'at_connect', self._on_capsule_player_collide), + ) + + self._flag_region_material = bs.Material() + self._flag_region_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ( + 'call', + 'at_connect', + babase.Call(self._handle_player_flag_region_collide, True), + ), + ( + 'call', + 'at_disconnect', + babase.Call(self._handle_player_flag_region_collide, False), + ), + ), + ) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SCARY + ) + + def get_instance_description(self) -> str | Sequence: + return description_ingame, self._score_to_win + + def get_instance_description_short(self) -> str | Sequence: + return description_short, self._score_to_win + + def create_team(self, sessionteam: bs.SessionTeam) -> Team: + return Team() + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + shared = SharedObjects.get() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = self._capsules_to_win * max( + 1, max(len(t.players) for t in self.teams) + ) + self._update_scoreboard() + + if isinstance(self.session, bs.FreeForAllSession): + self._flag_pos = self.map.get_flag_position(random.randint(0, 1)) + else: + self._flag_pos = self.map.get_flag_position(None) + + bs.timer(1.0, self._tick, repeat=True) + self._flag_state = FlagState.NEW + Flag.project_stand(self._flag_pos) + self._flag = Flag( + position=self._flag_pos, touchable=False, color=(1, 1, 1) + ) + self._flag_light = bs.newnode( + 'light', + attrs={ + 'position': self._flag_pos, + 'intensity': 0.2, + 'height_attenuated': False, + 'radius': 0.4, + 'color': (0.2, 0.2, 0.2), + }, + ) + # Flag region. + flagmats = [self._flag_region_material, shared.region_material] + bs.newnode( + 'region', + attrs={ + 'position': self._flag_pos, + 'scale': (1.8, 1.8, 1.8), + 'type': 'sphere', + 'materials': flagmats, + }, + ) + self._update_flag_state() + + def _tick(self) -> None: + self._update_flag_state() + + if self._scoring_team is None: + scoring_team = None + else: + scoring_team = self._scoring_team() + + if not scoring_team: + return + + if isinstance(self.session, bs.FreeForAllSession): + players = self.players + else: + players = scoring_team.players + + for player in players: + if player.time_at_flag > 0: + self.stats.player_scored( + player, 3, screenmessage=False, display=False + ) + if player.capsules > 0: + if self._flag_state != FlagState.HELD: + return + if scoring_team.score >= self._score_to_win: + return + + player.capsules -= 1 + scoring_team.score += 1 + self._handle_capsule_storage(( + self._flag_pos[0], + self._flag_pos[1]+1, + self._flag_pos[2] + ), player) + self._collect_sound.play(0.8, position=self._flag_pos) + + self._update_scoreboard() + if player.capsules > 0: + assert self._flag is not None + self._flag.set_score_text( + str(self._score_to_win - scoring_team.score)) + + # winner + if scoring_team.score >= self._score_to_win: + self.end_game() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results, announce_delay=0) + + def _update_flag_state(self) -> None: + holding_teams = set( + player.team for player in self.players if player.time_at_flag + ) + prev_state = self._flag_state + assert self._flag_light + assert self._flag is not None + assert self._flag.node + if len(holding_teams) > 1: + self._flag_state = FlagState.CONTESTED + self._scoring_team = None + self._flag_light.color = (0.6, 0.6, 0.1) + self._flag.node.color = (1.0, 1.0, 0.4) + elif len(holding_teams) == 1: + holding_team = list(holding_teams)[0] + self._flag_state = FlagState.HELD + self._scoring_team = weakref.ref(holding_team) + self._flag_light.color = babase.normalized_color(holding_team.color) + self._flag.node.color = holding_team.color + else: + self._flag_state = FlagState.UNCONTESTED + self._scoring_team = None + self._flag_light.color = (0.2, 0.2, 0.2) + self._flag.node.color = (1, 1, 1) + if self._flag_state != prev_state: + self._swipsound.play() + + def _handle_player_flag_region_collide(self, colliding: bool) -> None: + try: + spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True) + except bs.NotFoundError: + return + + if not spaz.is_alive(): + return + + player = spaz.getplayer(Player, True) + + # Different parts of us can collide so a single value isn't enough + # also don't count it if we're dead (flying heads shouldn't be able to + # win the game :-) + if colliding and player.is_alive(): + player.time_at_flag += 1 + else: + player.time_at_flag = max(0, player.time_at_flag - 1) + + self._update_flag_state() + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value( + team, team.score, self._score_to_win + ) + + def _drop_capsule(self, player: Player) -> None: + pt = player.node.position + + # Throw out capsules that the victim has + 2 more to keep the game running + for i in range(player.capsules + self._capsules_death): + # How far from each other these capsules should spawn + w = 0.6 + # How much these capsules should fly after spawning + s = 0.005 - (player.capsules * 0.01) + self._capsules.append( + Capsule( + position=(pt[0] + random.uniform(-w, w), + pt[1] + 0.75 + random.uniform(-w, w), + pt[2]), + velocity=(random.uniform(-s, s), + random.uniform(-s, s), + random.uniform(-s, s)), + lucky=False)) + if random.randint(1, 12) == 1 and self._lucky_capsules: + # How far from each other these capsules should spawn + w = 0.6 + # How much these capsules should fly after spawning + s = 0.005 + self._capsules.append( + Capsule( + position=(pt[0] + random.uniform(-w, w), + pt[1] + 0.75 + random.uniform(-w, w), + pt[2]), + velocity=(random.uniform(-s, s), + random.uniform(-s, s), + random.uniform(-s, s)), + lucky=True)) + + def _on_capsule_player_collide(self) -> None: + if self.has_ended(): + return + collision = bs.getcollision() + + # Be defensive here; we could be hitting the corpse of a player + # who just left/etc. + try: + capsule = collision.sourcenode.getdelegate(Capsule, True) + player = collision.opposingnode.getdelegate( + PlayerSpaz, True + ).getplayer(Player, True) + except bs.NotFoundError: + return + + if not player.is_alive(): + return + + if capsule.node.color_texture == self._capsule_lucky_tex: + player.capsules += 4 + PopupText( + bonus, + color=(1, 1, 0), + scale=1.5, + position=capsule.node.position + ).autoretain() + self._lucky_collect_sound.play(1.0, position=capsule.node.position) + bs.emitfx( + position=capsule.node.position, + velocity=(0, 0, 0), + count=int(6.4+random.random()*24), + scale=1.2, + spread=2.0, + chunk_type='spark') + bs.emitfx( + position=capsule.node.position, + velocity=(0, 0, 0), + count=int(4.0+random.random()*6), + emit_type='tendrils') + else: + player.capsules += 1 + self._collect_sound.play(0.6, position=capsule.node.position) + # create a flash + light = bs.newnode( + 'light', + attrs={ + 'position': capsule.node.position, + 'height_attenuated': False, + 'radius': 0.1, + 'color': (1, 1, 0)}) + + # Create a short text informing about your inventory + self._handle_capsule_storage(player.position, player) + + bs.animate(light, 'intensity', { + 0: 0, + 0.1: 0.5, + 0.2: 0 + }, loop=False) + bs.timer(0.2, light.delete) + capsule.handlemessage(bs.DieMessage()) + + def _update_player_light(self, player: Player, capsules: int) -> None: + if player.light: + intensity = 0.04 * capsules + bs.animate(player.light, 'intensity', { + 0.0: player.light.intensity, + 0.1: intensity + }) + + def newintensity(): + player.light.intensity = intensity + bs.timer(0.1, newintensity) + else: + player.light = bs.newnode( + 'light', + attrs={ + 'height_attenuated': False, + 'radius': 0.2, + 'intensity': 0.0, + 'color': (0.2, 1, 0.2) + }) + player.node.connectattr('position', player.light, 'position') + + def _handle_capsule_storage(self, pos: float, player: Player) -> None: + capsules = player.capsules + text = str(capsules) + scale = 1.75 + (0.02 * capsules) + if capsules > 10: + player.capsules = 10 + text = full_capacity + color = (1, 0.85, 0) + elif capsules > 7: + color = (1, 0, 0) + scale = 2.4 + elif capsules > 5: + color = (1, 0.4, 0.4) + scale = 2.1 + elif capsules > 3: + color = (1, 1, 0.4) + scale = 2.0 + else: + color = (1, 1, 1) + scale = 1.9 + PopupText( + text, + color=color, + scale=scale, + position=(pos[0], pos[1]-1, pos[2]) + ).autoretain() + self._update_player_light(player, capsules) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) # Augment default. + # No longer can count as time_at_flag once dead. + player = msg.getplayer(Player) + player.time_at_flag = 0 + self._update_flag_state() + self._drop_capsule(player) + player.capsules = 0 + self._update_player_light(player, 0) + self.respawn_player(player) + else: + return super().handlemessage(msg) + + +class Capsule(bs.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.5, 0.0), + lucky: bool = False): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # spawn just above the provided point + self._spawn_pos = (position[0], position[1], position[2]) + + if lucky: + activity._lucky_sound.play(1.0, self._spawn_pos) + + self.node = bs.newnode( + 'prop', + attrs={ + 'mesh': activity._capsule_mesh, + 'color_texture': activity._capsule_lucky_tex if lucky else ( + activity._capsule_tex), + 'body': 'crate' if lucky else 'capsule', + 'reflection': 'powerup' if lucky else 'soft', + 'body_scale': 0.65 if lucky else 0.3, + 'density': 6.0 if lucky else 4.0, + 'reflection_scale': [0.15], + 'shadow_size': 0.65 if lucky else 0.6, + 'position': self._spawn_pos, + 'velocity': velocity, + 'materials': [ + shared.object_material, activity._capsule_material] + }, + delegate=self) + bs.animate(self.node, 'mesh_scale', { + 0.0: 0.0, + 0.1: 0.9 if lucky else 0.6, + 0.16: 0.8 if lucky else 0.5 + }) + self._light_capsule = bs.newnode( + 'light', + attrs={ + 'position': self._spawn_pos, + 'height_attenuated': False, + 'radius': 0.5 if lucky else 0.1, + 'color': (0.2, 0.2, 0) if lucky else (0.2, 1, 0.2) + }) + self.node.connectattr('position', self._light_capsule, 'position') + + def handlemessage(self, msg: Any): + if isinstance(msg, bs.DieMessage): + self.node.delete() + bs.animate(self._light_capsule, 'intensity', { + 0: 1.0, + 0.05: 0.0 + }, loop=False) + bs.timer(0.05, self._light_capsule.delete) + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + elif isinstance(msg, bs.HitMessage): + self.node.handlemessage( + 'impulse', + msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0]/8, msg.velocity[1]/8, msg.velocity[2]/8, + 1.0*msg.magnitude, 1.0*msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + else: + return super().handlemessage(msg) diff --git a/plugins/minigames/demolition_war.py b/plugins/minigames/demolition_war.py new file mode 100644 index 000000000..7cce96cb2 --- /dev/null +++ b/plugins/minigames/demolition_war.py @@ -0,0 +1,305 @@ + +# ba_meta require api 9 +""" +DemolitionWar - BombFight on wooden floor flying in air. +Author: Mr.Smoothy +Discord: https://discord.gg/ucyaesh +Youtube: https://www.youtube.com/c/HeySmoothy +Website: https://bombsquad-community.web.app +Github: https://github.com/bombsquad-community +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from bascenev1lib.game.elimination import EliminationGame, Player +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.bomb import BombFactory +import random +from bascenev1lib.actor.playerspaz import PlayerSpaz +if TYPE_CHECKING: + from typing import Any + +# ba_meta export bascenev1.GameActivity + + +class DemolitionWar(EliminationGame): + name = 'DemolitionWar' + description = 'Last remaining alive wins.' + scoreconfig = bs.ScoreConfig( + label='Survived', scoretype=bs.ScoreType.SECONDS, none_is_winner=True + ) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Lives', default=False) + ) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Wooden Floor'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._lives_per_player = 1 + self._solo_mode = False + self._balance_total_lives = False + + def spawn_player(self, player: Player) -> bs.Actor: + p = [-6, -4.3, -2.6, -0.9, 0.8, 2.5, 4.2, 5.9] + q = [-4, -2.3, -0.6, 1.1, 2.8, 4.5] + + x = random.randrange(0, len(p)) + y = random.randrange(0, len(q)) + spaz = self.spawn_player_spaz(player, position=(p[x], 1.8, q[y])) + spaz.bomb_type = 'impact' + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player(enable_punch=False, + enable_bomb=True, + enable_pickup=True) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + def on_begin(self) -> None: + super().on_begin() + self.map_extend() + + def on_blast(self): + node = bs.getcollision().sourcenode + bs.emitfx((node.position[0], 0.9, node.position[2]), + (0, 2, 0), 30, 1, spread=1, chunk_type='splinter') + bs.timer(0.1, babase.Call(node.delete)) + + def map_extend(self): + # TODO need to improve here , so we can increase size of map easily with settings + p = [-6, -4.3, -2.6, -0.9, 0.8, 2.5, 4.2, 5.9] + q = [-4, -2.3, -0.6, 1.1, 2.8, 4.5] + factory = BombFactory.get() + self.ramp_bomb = bs.Material() + self.ramp_bomb.add_actions( + conditions=('they_have_material', factory.bomb_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True), + ('call', 'at_connect', babase.Call(self.on_blast)) + )) + self.ramps = [] + for i in p: + for j in q: + self.ramps.append(self.create_ramp(i, j)) + + def create_ramp(self, x, z): + + shared = SharedObjects.get() + self._real_collied_material = bs.Material() + + self._real_collied_material.add_actions( + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.mat = bs.Material() + self.mat.add_actions( + actions=(('modify_part_collision', 'physical', False), + ('modify_part_collision', 'collide', False)) + ) + pos = (x, 0, z) + ud_1_r = bs.newnode('region', attrs={'position': pos, 'scale': (1.5, 1, 1.5), 'type': 'box', 'materials': [ + shared.footing_material, self._real_collied_material, self.ramp_bomb]}) + + node = bs.newnode('prop', + owner=ud_1_r, + attrs={ + 'mesh': bs.getmesh('image1x1'), + 'light_mesh': bs.getmesh('powerupSimple'), + 'position': (2, 7, 2), + 'body': 'puck', + 'shadow_size': 0.0, + 'velocity': (0, 0, 0), + 'color_texture': bs.gettexture('tnt'), + 'mesh_scale': 1.5, + 'reflection_scale': [1.5], + 'materials': [self.mat, shared.object_material, shared.footing_material], + 'density': 9000000000 + }) + # node.changerotation(1, 0, 0) + mnode = bs.newnode('math', + owner=ud_1_r, + attrs={ + 'input1': (0, 0.6, 0), + 'operation': 'add' + }) + ud_1_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', node, 'position') + return ud_1_r + + +class mapdefs: + points = {} + # noinspection PyDictCreation + boxes = {} + boxes['area_of_interest_bounds'] = (0.0, 1.185751251, 0.4326226188) + ( + 0.0, 0.0, 0.0) + (29.8180273, 11.57249038, 18.89134176) + boxes['edge_box'] = (-0.103873591, 0.4133341891, 0.4294651013) + ( + 0.0, 0.0, 0.0) + (22.48295719, 1.290242794, 8.990252454) + points['ffa_spawn1'] = (-0.08015551329, 0.02275111462, + -4.373674593) + (8.895057015, 1.0, 0.444350722) + points['ffa_spawn2'] = (-0.08015551329, 0.02275111462, + 4.076288941) + (8.895057015, 1.0, 0.444350722) + points['flag1'] = (-10.99027878, 0.05744967453, 0.1095578275) + points['flag2'] = (11.01486398, 0.03986567039, 0.1095578275) + points['flag_default'] = (-0.1001374046, 0.04180340146, 0.1095578275) + boxes['goal1'] = (12.22454533, 1.0, + 0.1087926362) + (0.0, 0.0, 0.0) + (2.0, 2.0, 12.97466313) + boxes['goal2'] = (-12.15961605, 1.0, + 0.1097860203) + (0.0, 0.0, 0.0) + (2.0, 2.0, 13.11856424) + boxes['map_bounds'] = (0.0, 1.185751251, 0.4326226188) + (0.0, 0.0, 0.0) + ( + 42.09506485, 22.81173179, 29.76723155) + points['powerup_spawn1'] = (5.414681236, 0.9515026107, -5.037912441) + points['powerup_spawn2'] = (-5.555402285, 0.9515026107, -5.037912441) + points['powerup_spawn3'] = (5.414681236, 0.9515026107, 5.148223181) + points['powerup_spawn4'] = (-5.737266365, 0.9515026107, 5.148223181) + points['spawn1'] = (-10.03866341, 0.02275111462, 0.0) + (0.5, 1.0, 4.0) + points['spawn2'] = (9.823107149, 0.01092306765, 0.0) + (0.5, 1.0, 4.0) + points['tnt1'] = (-0.08421587483, 0.9515026107, -0.7762602271) + + +class WoodenFloor(bs._map.Map): # ahdunno if this is correct way, change if u find better way + """Stadium map for football games.""" + defs = mapdefs + defs.points['spawn1'] = (-12.03866341, 0.02275111462, 0.0) + (0.5, 1.0, 4.0) + defs.points['spawn2'] = (12.823107149, 0.01092306765, 0.0) + (0.5, 1.0, 4.0) + name = 'Wooden Floor' + + @classmethod + def get_play_types(cls) -> list[str]: + """Return valid play types for this map.""" + return ['melee', 'football', 'team_flag', 'keep_away'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'footballStadiumPreview' + + @classmethod + def on_preload(cls) -> Any: + data: dict[str, Any] = { + + 'mesh_bg': bs.getmesh('doomShroomBG'), + 'bg_vr_fill_mesh': bs.getmesh('natureBackgroundVRFill'), + 'collide_mesh': bs.getcollisionmesh('bridgitLevelCollide'), + 'tex': bs.gettexture('bridgitLevelColor'), + 'mesh_bg_tex': bs.gettexture('doomShroomBGColor'), + 'collide_bg': bs.getcollisionmesh('natureBackgroundCollide'), + 'railing_collide_mesh': + (bs.getcollisionmesh('bridgitLevelRailingCollide')), + 'bg_material': bs.Material() + } + data['bg_material'].add_actions(actions=('modify_part_collision', + 'friction', 10.0)) + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['mesh_bg'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['mesh_bg_tex'] + }) + self.vr = bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['bg_vr_fill_mesh'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['mesh_bg_tex'] + }) + gnode = bs.getactivity().globalsnode + gnode.tint = (1.3, 1.2, 1.0) + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + + def is_point_near_edge(self, + point: babase.Vec3, + running: bool = False) -> bool: + box_position = self.defs.boxes['edge_box'][0:3] + box_scale = self.defs.boxes['edge_box'][6:9] + xpos = (point.x - box_position[0]) / box_scale[0] + zpos = (point.z - box_position[2]) / box_scale[2] + return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 + + def _handle_player_collide(self): + try: + player = bs.getcollision().opposingnode.getdelegate( + PlayerSpaz, True) + except bs.NotFoundError: + return + if player.is_alive(): + player.shatter(True) + + +try: + bs._map.register_map(WoodenFloor) +except: + pass diff --git a/plugins/minigames/dodge_the_ball.py b/plugins/minigames/dodge_the_ball.py new file mode 100644 index 000000000..2bb1d9212 --- /dev/null +++ b/plugins/minigames/dodge_the_ball.py @@ -0,0 +1,768 @@ +""" + + DondgeTheBall minigame by EmperoR#4098 + +""" + +# Feel free to edit. + +# ba_meta require api 8 +from __future__ import annotations +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from random import choice +from enum import Enum +from bascenev1lib.actor.bomb import Blast +from bascenev1lib.actor.popuptext import PopupText +from bascenev1lib.actor.powerupbox import PowerupBox +from bascenev1lib.actor.onscreencountdown import OnScreenCountdown +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import NoReturn, Sequence, Any + + +# Type of ball in this game +class BallType(Enum): + """ Types of ball """ + EASY = 0 + # Decrease the next ball shooting speed(not ball speed). + # Ball color is yellow. + MEDIUM = 1 + # increase the next ball shooting speed(not ball speed). + # target the head of player. + # Ball color is purple. + HARD = 2 + # Target player according to player movement (not very accurate). + # Taget: player head. + # increase the next ball speed but less than MEDIUM. + # Ball color is crimson(purple+red = pinky color type). + + +# this dict decide the ball_type spawning rate like powerup box +ball_type_dict: dict[BallType, int] = { + BallType.EASY: 3, + BallType.MEDIUM: 2, + BallType.HARD: 1, +} + + +class Ball(bs.Actor): + """ Shooting Ball """ + + def __init__(self, + position: Sequence[float], + velocity: Sequence[float], + texture: babase.Texture, + body_scale: float = 1.0, + gravity_scale: float = 1.0, + ) -> NoReturn: + + super().__init__() + + shared = SharedObjects.get() + + ball_material = bs.Material() + ball_material.add_actions( + conditions=( + ( + ('we_are_younger_than', 100), + 'or', + ('they_are_younger_than', 100), + ), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'body': 'sphere', + 'position': position, + 'velocity': velocity, + 'body_scale': body_scale, + 'mesh': bs.getmesh('frostyPelvis'), + 'mesh_scale': body_scale, + 'color_texture': texture, + 'gravity_scale': gravity_scale, + 'density': 4.0, # increase density of ball so ball collide with player with heavy force. # ammm very bad grammer + 'materials': (ball_material,), + }, + ) + + # die the ball manually incase the ball doesn't fall the outside of the map + bs.timer(2.5, bs.WeakCall(self.handlemessage, bs.DieMessage())) + + # i am not handling anything in this ball Class(except for diemessage). + # all game things and logics going to be in the box class + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.DieMessage): + self.node.delete() + else: + super().handlemessage(msg) + + +class Box(bs.Actor): + """ A box that spawn midle of map as a decoration perpose """ + + def __init__(self, + position: Sequence[float], + velocity: Sequence[float], + ) -> NoReturn: + + super().__init__() + + shared = SharedObjects.get() + # self.ball_jump = 0.0; + no_hit_material = bs.Material() + # we don't need that the box was move and collide with objects. + no_hit_material.add_actions( + conditions=( + ('they_have_material', shared.pickup_material), + 'or', + ('they_have_material', shared.attack_material), + ), + actions=('modify_part_collision', 'collide', False), + ) + + no_hit_material.add_actions( + conditions=( + ('they_have_material', shared.object_material), + 'or', + ('they_dont_have_material', shared.footing_material), + ), + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False), + ), + ) + + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'body': 'box', + 'position': position, + 'mesh': bs.getmesh('powerup'), + 'light_mesh': bs.getmesh('powerupSimple'), + 'shadow_size': 0.5, + 'body_scale': 1.4, + 'mesh_scale': 1.4, + 'color_texture': bs.gettexture('landMineLit'), + 'reflection': 'powerup', + 'reflection_scale': [1.0], + 'materials': (no_hit_material,), + }, + ) + # light + self.light = bs.newnode( + "light", + owner=self.node, + attrs={ + 'radius': 0.2, + 'intensity': 0.8, + 'color': (0.0, 1.0, 0.0), + } + ) + self.node.connectattr("position", self.light, "position") + # Drawing circle and circleOutline in radius of 3, + # so player can see that how close he is to the box. + # If player is inside this circle the ball speed will increase. + circle = bs.newnode( + "locator", + owner=self.node, + attrs={ + 'shape': 'circle', + 'color': (1.0, 0.0, 0.0), + 'opacity': 0.1, + 'size': (6.0, 0.0, 6.0), + 'draw_beauty': False, + 'additive': True, + }, + ) + self.node.connectattr("position", circle, "position") + # also adding a outline cause its look nice. + circle_outline = bs.newnode( + "locator", + owner=self.node, + attrs={ + 'shape': 'circleOutline', + 'color': (1.0, 1.0, 0.0), + 'opacity': 0.1, + 'size': (6.0, 0.0, 6.0), + 'draw_beauty': False, + 'additive': True, + }, + ) + self.node.connectattr("position", circle_outline, "position") + + # all ball attribute that we need. + self.ball_type: BallType = BallType.EASY + self.shoot_timer: bs.Timer | None = None + self.shoot_speed: float = 0.0 + # this force the shoot if player is inside the red circle. + self.force_shoot_speed: float = 0.0 + self.ball_mag = 3000 + self.ball_gravity: float = 1.0 + self.ball_tex: babase.Texture | None = None + # only for Hard ball_type + self.player_facing_direction: list[float, float] = [0.0, 0.0] + # ball shoot soound. + self.shoot_sound = bs.getsound('laserReverse') + + # same as "powerupdist" + self.ball_type_dist: list[BallType] = [] + + for ball in ball_type_dict: + for _ in range(ball_type_dict[ball]): + self.ball_type_dist.append(ball) + + # Here main logic of game goes here. + # like shoot balls, shoot speed, anything we want goes here(except for some thing). + def start_shoot(self) -> NoReturn: + + # getting all allive players in a list. + alive_players_list = self.activity.get_alive_players() + + # make sure that list is not Empty. + if len(alive_players_list) > 0: + + # choosing a random player from list. + target_player = choice(alive_players_list) + # highlight the target player + self.highlight_target_player(target_player) + + # to finding difference between player and box. + # we just need to subtract player pos and ball pos. + # Same logic as eric applied in Target Practice Gamemode. + difference = babase.Vec3(target_player.position) - babase.Vec3(self.node.position) + + # discard Y position so ball shoot more straight. + difference[1] = 0.0 + + # and now, this length method returns distance in float. + # we're gonna use this value for calculating player analog stick + distance = difference.length() + + # shoot a random BallType + self.upgrade_ball_type(choice(self.ball_type_dist)) + + # and check the ball_type and upgrade it gravity_scale, texture, next ball speed. + self.check_ball_type(self.ball_type) + + # For HARD ball i am just focusing on player analog stick facing direction. + # Not very accurate and that's we need. + if self.ball_type == BallType.HARD: + self.calculate_player_analog_stick(target_player, distance) + else: + self.player_facing_direction = [0.0, 0.0] + + pos = self.node.position + + if self.ball_type == BallType.MEDIUM or self.ball_type == BallType.HARD: + # Target head by increasing Y pos. + # How this work? cause ball gravity_scale is ...... + pos = (pos[0], pos[1]+.25, pos[2]) + + # ball is generating.. + ball = Ball( + position=pos, + velocity=(0.0, 0.0, 0.0), + texture=self.ball_tex, + gravity_scale=self.ball_gravity, + body_scale=1.0, + ).autoretain() + + # shoot Animation and sound. + self.shoot_animation() + + # force the shoot speed if player try to go inside the red circle. + if self.force_shoot_speed != 0.0: + self.shoot_speed = self.force_shoot_speed + + # push the ball to the player + ball.node.handlemessage( + 'impulse', + self.node.position[0], # ball spawn position X + self.node.position[1], # Y + self.node.position[2], # Z + 0, 0, 0, # velocity x,y,z + self.ball_mag, # magnetude + 0.000, # magnetude velocity + 0.000, # radius + 0.000, # idk + difference[0] + self.player_facing_direction[0], # force direction X + difference[1], # force direction Y + difference[2] + self.player_facing_direction[1], # force direction Z + ) + # creating our timer and shoot the ball again.(and we create a loop) + self.shoot_timer = bs.Timer(self.shoot_speed, self.start_shoot) + + def upgrade_ball_type(self, ball_type: BallType) -> NoReturn: + + self.ball_type = ball_type + + def check_ball_type(self, ball_type: BallType) -> NoReturn: + + if ball_type == BallType.EASY: + self.shoot_speed = 0.8 + self.ball_gravity = 1.0 + # next ball shoot speed + self.ball_mag = 3000 + # box light color and ball tex + self.light.color = (1.0, 1.0, 0.0) + self.ball_tex = bs.gettexture('egg4') + elif ball_type == BallType.MEDIUM: + self.ball_mag = 3000 + # decrease the gravity scale so, ball shoot without falling and straight. + self.ball_gravity = 0.0 + # next ball shoot speed. + self.shoot_speed = 0.4 + # box light color and ball tex. + self.light.color = (1.0, 0.0, 1.0) + self.ball_tex = bs.gettexture('egg3') + elif ball_type == BallType.HARD: + self.ball_mag = 2500 + self.ball_gravity = 0.0 + # next ball shoot speed. + self.shoot_speed = 0.6 + # box light color and ball tex. + self.light.color = (1.0, 0.2, 1.0) + self.ball_tex = bs.gettexture('egg1') + + def shoot_animation(self) -> NoReturn: + + bs.animate( + self.node, + "mesh_scale", { + 0.00: 1.4, + 0.05: 1.7, + 0.10: 1.4, + } + ) + # playing shoot sound. + # self.shoot_sound, position = self.node.position.play(); + self.shoot_sound.play() + + def highlight_target_player(self, player: bs.Player) -> NoReturn: + + # adding light + light = bs.newnode( + "light", + owner=self.node, + attrs={ + 'radius': 0.0, + 'intensity': 1.0, + 'color': (1.0, 0.0, 0.0), + } + ) + bs.animate( + light, + "radius", { + 0.05: 0.02, + 0.10: 0.07, + 0.15: 0.15, + 0.20: 0.13, + 0.25: 0.10, + 0.30: 0.05, + 0.35: 0.02, + 0.40: 0.00, + } + ) + # And a circle outline with ugly animation. + circle_outline = bs.newnode( + "locator", + owner=player.actor.node, + attrs={ + 'shape': 'circleOutline', + 'color': (1.0, 0.0, 0.0), + 'opacity': 1.0, + 'draw_beauty': False, + 'additive': True, + }, + ) + bs.animate_array( + circle_outline, + 'size', + 1, { + 0.05: [0.5], + 0.10: [0.8], + 0.15: [1.5], + 0.20: [2.0], + 0.25: [1.8], + 0.30: [1.3], + 0.35: [0.6], + 0.40: [0.0], + } + ) + + # coonect it and... + player.actor.node.connectattr("position", light, "position") + player.actor.node.connectattr("position", circle_outline, "position") + + # immediately delete the node after another player has been targeted. + self.shoot_speed = 0.5 if self.shoot_speed == 0.0 else self.shoot_speed + bs.timer(self.shoot_speed, light.delete) + bs.timer(self.shoot_speed, circle_outline.delete) + + def calculate_player_analog_stick(self, player: bs.Player, distance: float) -> NoReturn: + # at first i was very confused how i can read the player analog stick \ + # then i saw TheMikirog#1984 autorun plugin code. + # and i got it how analog stick values are works. + # just need to store analog stick facing direction and need some calculation according how far player pushed analog stick. + # Notice that how vertical direction is inverted, so we need to put a minus infront of veriable.(so ball isn't shoot at wrong direction). + self.player_facing_direction[0] = player.actor.node.move_left_right + self.player_facing_direction[1] = -player.actor.node.move_up_down + + # if player is too close and the player pushing his analog stick fully the ball shoot's too far away to player. + # so, we need to reduce the value of "self.player_facing_direction" to fix this problem. + if distance <= 3: + self.player_facing_direction[0] = 0.4 if self.player_facing_direction[0] > 0 else -0.4 + self.player_facing_direction[1] = 0.4 if self.player_facing_direction[0] > 0 else -0.4 + # same problem to long distance but in reverse, the ball can't reach to the player, + # its because player analog stick value is between 1 and -1, + # and this value is low to shoot ball forward to Player if player is too far from the box. + # so. let's increase to 1.5 if player pushed analog stick fully. + elif distance > 6.5: + # So many calculation according to how analog stick pushed by player. + # Horizontal(left-right) calculation + if self.player_facing_direction[0] > 0.4: + self.player_facing_direction[0] = 1.5 + elif self.player_facing_direction[0] < -0.4: + self.player_facing_direction[0] = -1.5 + else: + if self.player_facing_direction[0] > 0.0: + self.player_facing_direction[0] = 0.2 + elif self.player_facing_direction[0] < 0.0: + self.player_facing_direction[0] = -0.2 + else: + self.player_facing_direction[0] = 0.0 + + # Vertical(up-down) calculation. + if self.player_facing_direction[1] > 0.4: + self.player_facing_direction[1] = 1.5 + elif self.player_facing_direction[1] < -0.4: + self.player_facing_direction[1] = -1.5 + else: + if self.player_facing_direction[1] > 0.0: + self.player_facing_direction[1] = 0.2 + elif self.player_facing_direction[1] < 0.0: + self.player_facing_direction[1] = -0.2 + else: + self.player_facing_direction[1] = -0.0 + + # if we want stop the ball shootes + def stop_shoot(self) -> NoReturn: + # Kill the timer. + self.shoot_timer = None + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + +# almost 80 % for game we done in box class. +# now remain things, like name, seetings, scoring, cooldonw, +# and main thing don't allow player to camp inside of box are going in this class. + +# ba_meta export bascenev1.GameActivity + + +class DodgeTheBall(bs.TeamGameActivity[Player, Team]): + + # defining name, description and settings.. + name = 'Dodge the ball' + description = 'Survive from shooting balls' + + available_settings = [ + bs.IntSetting( + 'Cooldown', + min_value=20, + default=45, + increment=5, + ), + bs.BoolSetting('Epic Mode', default=False) + ] + + # Don't allow joining after we start. + allow_mid_activity_joins = False + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + # We support team and ffa sessions. + return issubclass(sessiontype, bs.FreeForAllSession) or issubclass( + sessiontype, bs.DualTeamSession, + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + # This Game mode need a flat and perfect shape map where can player fall outside map. + # bombsquad have "Doom Shroom" map. + # Not perfect map for this game mode but its fine for this gamemode. + # the problem is that Doom Shroom is not a perfect circle and not flat also. + return ['Doom Shroom'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._epic_mode = bool(settings['Epic Mode']) + self.countdown_time = int(settings['Cooldown']) + + self.check_player_pos_timer: bs.Timer | None = None + self.shield_drop_timer: bs.Timer | None = None + # cooldown and Box + self._countdown: OnScreenCountdown | None = None + self.box: Box | None = None + + # this lists for scoring. + self.joined_player_list: list[bs.Player] = [] + self.dead_player_list: list[bs.Player] = [] + + # normally play RUN AWAY music cause is match with our gamemode at.. my point, + # but in epic switch to EPIC. + self.slow_motion = self._epic_mode + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.RUN_AWAY + ) + + def get_instance_description(self) -> str | Sequence: + return 'Keep away as possible as you can' + + # add a tiny text under our game name. + def get_instance_description_short(self) -> str | Sequence: + return 'Dodge the shooting balls' + + def on_begin(self) -> NoReturn: + super().on_begin() + + # spawn our box at middle of the map + self.box = Box( + position=(0.5, 2.7, -3.9), + velocity=(0.0, 0.0, 0.0), + ).autoretain() + + # create our cooldown + self._countdown = OnScreenCountdown( + duration=self.countdown_time, + endcall=self.play_victory_sound_and_end, + ) + + # and starts the cooldown and shootes. + bs.timer(5.0, self._countdown.start) + bs.timer(5.0, self.box.start_shoot) + + # start checking all player pos. + bs.timer(5.0, self.check_player_pos) + + # drop shield every ten Seconds + # need five seconds delay Because shootes start after 5 seconds. + bs.timer(15.0, self.drop_shield) + + # This function returns all alive players in game. + # i thinck you see this function in Box class. + def get_alive_players(self) -> Sequence[bs.Player]: + + alive_players = [] + + for team in self.teams: + for player in team.players: + if player.is_alive(): + alive_players.append(player) + + return alive_players + + # let's disallowed camping inside of box by doing a blast and increasing ball shoot speed. + def check_player_pos(self): + + for player in self.get_alive_players(): + + # same logic as applied for the ball + difference = babase.Vec3(player.position) - babase.Vec3(self.box.node.position) + + distance = difference.length() + + if distance < 3: + self.box.force_shoot_speed = 0.2 + else: + self.box.force_shoot_speed = 0.0 + + if distance < 0.5: + Blast( + position=self.box.node.position, + velocity=self.box.node.velocity, + blast_type='normal', + blast_radius=1.0, + ).autoretain() + + PopupText( + position=self.box.node.position, + text='Keep away from me', + random_offset=0.0, + scale=2.0, + color=self.box.light.color, + ).autoretain() + + # create our timer and start looping it + self.check_player_pos_timer = bs.Timer(0.1, self.check_player_pos) + + # drop useless shield's too give player temptation. + def drop_shield(self) -> NoReturn: + + pos = self.box.node.position + + PowerupBox( + position=(pos[0] + 4.0, pos[1] + 3.0, pos[2]), + poweruptype='shield', + ).autoretain() + + PowerupBox( + position=(pos[0] - 4.0, pos[1] + 3.0, pos[2]), + poweruptype='shield', + ).autoretain() + + self.shield_drop_timer = bs.Timer(10.0, self.drop_shield) + + # when cooldown time up i don't want that the game end immediately. + def play_victory_sound_and_end(self) -> NoReturn: + + # kill timers + self.box.stop_shoot() + self.check_player_pos_timer = None + self.shield_drop_timer = None + + bs.timer(2.0, self.end_game) + + # this function runs when A player spawn in map + def spawn_player(self, player: Player) -> NoReturn: + spaz = self.spawn_player_spaz(player) + + # reconnect this player's controls. + # without bomb, punch and pickup. + spaz.connect_controls_to_player( + enable_punch=False, enable_bomb=False, enable_pickup=False, + ) + + # storing all players for ScorinG. + self.joined_player_list.append(player) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + + # very helpful function to check end game when player dead or leav. + def _check_end_game(self) -> bool: + + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + if living_team_count <= 0: + # kill the coutdown timer incase the all players dead before game is about to going to be end. + # so, countdown won't call the function. + # FIXE ME: it's that ok to kill this timer? + # self._countdown._timer = None; + self.end_game() + + # this function called when player leave. + def on_player_leave(self, player: Player) -> NoReturn: + # Augment default behavior. + super().on_player_leave(player) + + # checking end game. + self._check_end_game() + + # this gamemode needs to handle only one msg "PlayerDiedMessage". + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + # and storing the dead player records in our dead_player_list. + self.dead_player_list.append(msg.getplayer(Player)) + + # check the end game. + bs.timer(1.0, self._check_end_game) + + def end_game(self): + # kill timers + self.box.stop_shoot() + self.check_player_pos_timer = None + self.shield_drop_timer = None + + # here the player_dead_list and joined_player_list gonna be very helpful. + for team in self.teams: + for player in team.players: + + # for scoring i am just following the index of the player_dead_list. + # for dead list... + # 0th index player dead first. + # 1st index player dead second. + # and so on... + # i think you got it... maybe + # sometime we also got a empty list + # if we got a empty list that means all players are survived or maybe only one player playing and he/she survived. + if len(self.dead_player_list) > 0: + + for index, dead_player in enumerate(self.dead_player_list): + # if this condition is true we find the dead player \ + # and his index with enumerate function. + if player == dead_player: + # updating with one, because i don't want to give 0 score to first dead player. + index += 1 + break + # and if this statement is true we just find a survived player. + # for survived player i am giving the highest score according to how many players are joined. + elif index == len(self.dead_player_list) - 1: + index = len(self.joined_player_list) + # for survived player i am giving the highest score according to how many players are joined. + else: + index = len(self.joined_player_list) + + # and here i am following Table of 10 for scoring. + # very lazY. + score = int(10 * index) + + self.stats.player_scored(player, score, screenmessage=False) + + # Ok now calc game results: set a score for each team and then tell \ + # the game to end. + results = bs.GameResults() + + # Remember that 'free-for-all' mode is simply a special form \ + # of 'teams' mode where each player gets their own team, so we can \ + # just always deal in teams and have all cases covered. + # hmmm... some eric comments might be helpful to you. + for team in self.teams: + + max_index = 0 + for player in team.players: + # for the team, we choose only one player who survived longest. + # same logic.. + if len(self.dead_player_list) > 0: + for index, dead_player in enumerate(self.dead_player_list): + if player == dead_player: + index += 1 + break + elif index == len(self.dead_player_list) - 1: + index = len(self.joined_player_list) + else: + index = len(self.joined_player_list) + + max_index = max(max_index, index) + # set the team score + results.set_team_score(team, int(10 * max_index)) + # and end the game + self.end(results=results) diff --git a/plugins/minigames/down_into_the_abyss.py b/plugins/minigames/down_into_the_abyss.py new file mode 100644 index 000000000..ab26ef589 --- /dev/null +++ b/plugins/minigames/down_into_the_abyss.py @@ -0,0 +1,790 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import random +from bascenev1._map import register_map +from bascenev1lib.actor.spaz import PickupMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.spazbot import SpazBotSet, ChargerBotPro, TriggerBotPro +from bascenev1lib.actor.bomb import Blast +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = bs.app.lang.language + +if lang == 'Spanish': + name = 'Abajo en el Abismo' + description = 'Sobrevive tanto como puedas' + help = 'El mapa es 3D, ¡ten cuidado!' + author = 'Autor: Deva' + github = 'GitHub: spdv123' + blog = 'Blog: superdeva.info' + peaceTime = 'Tiempo de Paz' + npcDensity = 'Densidad de Enemigos' + hint_use_punch = '¡Ahora puedes golpear a los enemigos!' +elif lang == 'Chinese': + name = '无尽深渊' + description = '在无穷尽的坠落中存活更长时间' + help = '' + author = '作者: Deva' + github = 'GitHub: spdv123' + blog = '博客: superdeva.info' + peaceTime = '和平时间' + npcDensity = 'NPC密度' + hint_use_punch = u'现在可以使用拳头痛扁你的敌人了' +else: + name = 'Down Into The Abyss' + description = 'Survive as long as you can' + help = 'The map is 3D, be careful!' + author = 'Author: Deva' + github = 'GitHub: spdv123' + blog = 'Blog: superdeva.info' + peaceTime = 'Peace Time' + npcDensity = 'NPC Density' + hint_use_punch = 'You can punch your enemies now!' + + +class AbyssMap(bs.Map): + from bascenev1lib.mapdata import happy_thoughts as defs + # Add the y-dimension space for players + defs.boxes['map_bounds'] = (-0.8748348681, 9.212941713, -9.729538885) \ + + (0.0, 0.0, 0.0) \ + + (36.09666006, 26.19950145, 20.89541168) + name = 'Abyss Unhappy' + + @classmethod + def get_play_types(cls) -> list[str]: + """Return valid play types for this map.""" + return ['abyss'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'alwaysLandPreview' + + @classmethod + def on_preload(cls) -> Any: + data: dict[str, Any] = { + 'mesh': bs.getmesh('alwaysLandLevel'), + 'bottom_mesh': bs.getmesh('alwaysLandLevelBottom'), + 'bgmesh': bs.getmesh('alwaysLandBG'), + 'collision_mesh': bs.getcollisionmesh('alwaysLandLevelCollide'), + 'tex': bs.gettexture('alwaysLandLevelColor'), + 'bgtex': bs.gettexture('alwaysLandBGColor'), + 'vr_fill_mound_mesh': bs.getmesh('alwaysLandVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound') + } + return data + + @classmethod + def get_music_type(cls) -> bs.MusicType: + return bs.MusicType.FLYING + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, -3.7, 2.5)) + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['vr_fill_mound_mesh'], + 'lighting': False, + 'vr_only': True, + 'color': (0.2, 0.25, 0.2), + 'background': True, + 'color_texture': self.preloaddata['vr_fill_mound_tex'] + }) + gnode = bs.getactivity().globalsnode + gnode.happy_thoughts_mode = True + gnode.shadow_offset = (0.0, 8.0, 5.0) + gnode.tint = (1.3, 1.23, 1.0) + gnode.ambient_color = (1.3, 1.23, 1.0) + gnode.vignette_outer = (0.64, 0.59, 0.69) + gnode.vignette_inner = (0.95, 0.95, 0.93) + gnode.vr_near_clip = 1.0 + self.is_flying = True + + +register_map(AbyssMap) + + +class SpazTouchFoothold: + pass + + +class BombToDieMessage: + pass + + +class Foothold(bs.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + power: str = 'random', + size: float = 6.0, + breakable: bool = True, + moving: bool = False): + super().__init__() + shared = SharedObjects.get() + powerup = PowerupBoxFactory.get() + + fmesh = bs.getmesh('landMine') + fmeshs = bs.getmesh('powerupSimple') + self.died = False + self.breakable = breakable + self.moving = moving # move right and left + self.lrSig = 1 # left or right signal + self.lrSpeedPlus = random.uniform(1 / 2.0, 1 / 0.7) + self._npcBots = SpazBotSet() + + self.foothold_material = bs.Material() + self.impact_sound = bui.getsound('impactMedium') + + self.foothold_material.add_actions( + conditions=(('they_dont_have_material', shared.player_material), + 'and', + ('they_have_material', shared.object_material), + 'or', + ('they_have_material', shared.footing_material)), + actions=(('modify_node_collision', 'collide', True), + )) + + self.foothold_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('modify_part_collision', 'physical', True), + ('modify_part_collision', 'stiffness', 0.05), + ('message', 'our_node', 'at_connect', SpazTouchFoothold()), + )) + + self.foothold_material.add_actions( + conditions=('they_have_material', self.foothold_material), + actions=('modify_node_collision', 'collide', False), + ) + + tex = { + 'punch': powerup.tex_punch, + 'sticky_bombs': powerup.tex_sticky_bombs, + 'ice_bombs': powerup.tex_ice_bombs, + 'impact_bombs': powerup.tex_impact_bombs, + 'health': powerup.tex_health, + 'curse': powerup.tex_curse, + 'shield': powerup.tex_shield, + 'land_mines': powerup.tex_land_mines, + 'tnt': bs.gettexture('tnt'), + }.get(power, bs.gettexture('tnt')) + + powerupdist = { + powerup.tex_bomb: 3, + powerup.tex_ice_bombs: 2, + powerup.tex_punch: 3, + powerup.tex_impact_bombs: 3, + powerup.tex_land_mines: 3, + powerup.tex_sticky_bombs: 4, + powerup.tex_shield: 4, + powerup.tex_health: 3, + powerup.tex_curse: 1, + bs.gettexture('tnt'): 2 + } + + self.randtex = [] + + for keyTex in powerupdist: + for i in range(powerupdist[keyTex]): + self.randtex.append(keyTex) + + if power == 'random': + random.seed() + tex = random.choice(self.randtex) + + self.tex = tex + self.powerup_type = { + powerup.tex_punch: 'punch', + powerup.tex_bomb: 'triple_bombs', + powerup.tex_ice_bombs: 'ice_bombs', + powerup.tex_impact_bombs: 'impact_bombs', + powerup.tex_land_mines: 'land_mines', + powerup.tex_sticky_bombs: 'sticky_bombs', + powerup.tex_shield: 'shield', + powerup.tex_health: 'health', + powerup.tex_curse: 'curse', + bs.gettexture('tnt'): 'tnt' + }.get(self.tex, '') + + self._spawn_pos = (position[0], position[1], position[2]) + + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'body': 'landMine', + 'position': self._spawn_pos, + 'mesh': fmesh, + 'light_mesh': fmeshs, + 'shadow_size': 0.5, + 'velocity': (0, 0, 0), + 'density': 90000000000, + 'sticky': False, + 'body_scale': size, + 'mesh_scale': size, + 'color_texture': tex, + 'reflection': 'powerup', + 'is_area_of_interest': True, + 'gravity_scale': 0.0, + 'reflection_scale': [0], + 'materials': [self.foothold_material, + shared.object_material, + shared.footing_material] + }) + self.touchedSpazs = set() + self.keep_vel() + + def keep_vel(self) -> None: + if self.node and not self.died: + speed = bs.getactivity().cur_speed + if self.moving: + if abs(self.node.position[0]) > 10: + self.lrSig *= -1 + self.node.velocity = ( + self.lrSig * speed * self.lrSpeedPlus, speed, 0) + bs.timer(0.1, bs.WeakCall(self.keep_vel)) + else: + self.node.velocity = (0, speed, 0) + # self.node.extraacceleration = (0, self.speed, 0) + bs.timer(0.1, bs.WeakCall(self.keep_vel)) + + def tnt_explode(self) -> None: + pos = self.node.position + Blast(position=pos, + blast_radius=6.0, + blast_type='tnt', + source_player=None).autoretain() + + def spawn_npc(self) -> None: + if not self.breakable: + return + if self._npcBots.have_living_bots(): + return + if random.randint(0, 3) >= bs.getactivity().npc_density: + return + pos = self.node.position + pos = (pos[0], pos[1] + 1, pos[2]) + self._npcBots.spawn_bot( + bot_type=random.choice([ChargerBotPro, TriggerBotPro]), + pos=pos, + spawn_time=10) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + self.died = True + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + elif isinstance(msg, BombToDieMessage): + if self.powerup_type == 'tnt': + self.tnt_explode() + self.handlemessage(bs.DieMessage()) + elif isinstance(msg, bs.HitMessage): + ispunched = (msg.srcnode and msg.srcnode.getnodetype() == 'spaz') + if not ispunched: + if self.breakable: + self.handlemessage(BombToDieMessage()) + elif isinstance(msg, SpazTouchFoothold): + node = bs.getcollision().opposingnode + if node is not None and node: + try: + spaz = node.getdelegate(object) + if not isinstance(spaz, AbyssPlayerSpaz): + return + if spaz in self.touchedSpazs: + return + self.touchedSpazs.add(spaz) + self.spawn_npc() + spaz.fix_2D_position() + if self.powerup_type not in ['', 'tnt']: + node.handlemessage( + bs.PowerupMessage(self.powerup_type)) + except Exception as e: + print(e) + pass + + +class AbyssPlayerSpaz(PlayerSpaz): + + def __init__(self, + player: bs.Player, + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + character: str = 'Spaz', + powerups_expire: bool = True): + super().__init__(player=player, + color=color, + highlight=highlight, + character=character, + powerups_expire=powerups_expire) + self.node.fly = False + self.node.hockey = True + self.hitpoints_max = self.hitpoints = 1500 # more HP to handle drop + bs.timer(bs.getactivity().peace_time, + bs.WeakCall(self.safe_connect_controls_to_player)) + + def safe_connect_controls_to_player(self) -> None: + try: + self.connect_controls_to_player() + except: + pass + + def on_move_up_down(self, value: float) -> None: + """ + Called to set the up/down joystick amount on this spaz; + used for player or AI connections. + value will be between -32768 to 32767 + WARNING: deprecated; use on_move instead. + """ + if not self.node: + return + if self.node.run > 0.1: + self.node.move_up_down = value + else: + self.node.move_up_down = value / 3. + + def on_move_left_right(self, value: float) -> None: + """ + Called to set the left/right joystick amount on this spaz; + used for player or AI connections. + value will be between -32768 to 32767 + WARNING: deprecated; use on_move instead. + """ + if not self.node: + return + if self.node.run > 0.1: + self.node.move_left_right = value + else: + self.node.move_left_right = value / 1.5 + + def fix_2D_position(self) -> None: + self.node.fly = True + bs.timer(0.02, bs.WeakCall(self.disable_fly)) + + def disable_fly(self) -> None: + if self.node: + self.node.fly = False + + def curse(self) -> None: + """ + Give this poor spaz a curse; + he will explode in 5 seconds. + """ + if not self._cursed: + factory = SpazFactory.get() + self._cursed = True + + # Add the curse material. + for attr in ['materials', 'roller_materials']: + materials = getattr(self.node, attr) + if factory.curse_material not in materials: + setattr(self.node, attr, + materials + (factory.curse_material, )) + + # None specifies no time limit + assert self.node + if self.curse_time == -1: + self.node.curse_death_time = -1 + else: + # Note: curse-death-time takes milliseconds. + tval = bs.time() + assert isinstance(tval, (float, int)) + self.node.curse_death_time = bs.time() + 15 + bs.timer(15, bs.WeakCall(self.curse_explode)) + + def handlemessage(self, msg: Any) -> Any: + dontUp = False + + if isinstance(msg, PickupMessage): + dontUp = True + collision = bs.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + + if opposingnode is None or not opposingnode: + return True + opposingdelegate = opposingnode.getdelegate(object) + # Don't pick up the foothold + if isinstance(opposingdelegate, Foothold): + return True + + # dont allow picking up of invincible dudes + try: + if opposingnode.invincible: + return True + except Exception: + pass + + # if we're grabbing the pelvis of a non-shattered spaz, + # we wanna grab the torso instead + if (opposingnode.getnodetype() == 'spaz' + and not opposingnode.shattered and opposingbody == 4): + opposingbody = 1 + + # Special case - if we're holding a flag, don't replace it + # (hmm - should make this customizable or more low level). + held = self.node.hold_node + if held and held.getnodetype() == 'flag': + return True + + # Note: hold_body needs to be set before hold_node. + self.node.hold_body = opposingbody + self.node.hold_node = opposingnode + + if not dontUp: + PlayerSpaz.handlemessage(self, msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: float | None = None + self.notIn: bool = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class AbyssGame(bs.TeamGameActivity[Player, Team]): + + name = name + description = description + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.MILLISECONDS, + version='B') + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + + # We're currently hard-coded for one map. + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Abyss Unhappy'] + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.FloatChoiceSetting( + peaceTime, + choices=[ + ('None', 0.0), + ('Shorter', 2.5), + ('Short', 5.0), + ('Normal', 10.0), + ('Long', 15.0), + ('Longer', 20.0), + ], + default=10.0, + ), + bs.FloatChoiceSetting( + npcDensity, + choices=[ + ('0%', 0), + ('25%', 1), + ('50%', 2), + ('75%', 3), + ('100%', 4), + ], + default=2, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession) + or issubclass(sessiontype, bs.CoopSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + self._epic_mode = settings.get('Epic Mode', False) + self._last_player_death_time: float | None = None + self._timer: OnScreenTimer | None = None + self.fix_y = -5.614479365 + self.start_z = 0 + self.init_position = (0, self.start_z, self.fix_y) + self.team_init_positions = [(-5, self.start_z, self.fix_y), + (5, self.start_z, self.fix_y)] + self.cur_speed = 2.5 + # TODO: The variable below should be set in settings + self.peace_time = float(settings[peaceTime]) + self.npc_density = float(settings[npcDensity]) + + # Some base class overrides: + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + if self._epic_mode: + self.slow_motion = True + + self._game_credit = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'v_attach': 'bottom', + 'h_align': 'center', + 'vr_depth': 0, + 'color': (0.0, 0.7, 1.0), + 'shadow': 1.0 if True else 0.5, + 'flatness': 1.0 if True else 0.5, + 'position': (0, 0), + 'scale': 0.8, + 'text': ' | '.join([author, github, blog]) + })) + + def get_instance_description(self) -> str | Sequence: + return description + + def get_instance_description_short(self) -> str | Sequence: + return self.get_instance_description() + '\n' + help + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + player.notIn = True + bs.broadcastmessage(babase.Lstr( + resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0)) + self.spawn_player(player) + + def on_begin(self) -> None: + super().on_begin() + self._timer = OnScreenTimer() + self._timer.start() + + self.level_cnt = 1 + + if self.teams_or_ffa() == 'teams': + ip0 = self.team_init_positions[0] + ip1 = self.team_init_positions[1] + Foothold( + (ip0[0], ip0[1] - 2, ip0[2]), + power='shield', breakable=False).autoretain() + Foothold( + (ip1[0], ip1[1] - 2, ip1[2]), + power='shield', breakable=False).autoretain() + else: + ip = self.init_position + Foothold( + (ip[0], ip[1] - 2, ip[2]), + power='shield', breakable=False).autoretain() + + bs.timer(int(5.0 / self.cur_speed), + bs.WeakCall(self.add_foothold), repeat=True) + + # Repeat check game end + bs.timer(1.0, self._check_end_game, repeat=True) + bs.timer(self.peace_time + 0.1, + bs.WeakCall(self.tip_hint, hint_use_punch)) + bs.timer(6.0, bs.WeakCall(self.faster_speed), repeat=True) + + def tip_hint(self, text: str) -> None: + bs.broadcastmessage(text, color=(0.2, 0.2, 1)) + + def faster_speed(self) -> None: + self.cur_speed *= 1.15 + + def add_foothold(self) -> None: + ip = self.init_position + ip_1 = (ip[0] - 7, ip[1], ip[2]) + ip_2 = (ip[0] + 7, ip[1], ip[2]) + ru = random.uniform + self.level_cnt += 1 + if self.level_cnt % 3: + Foothold(( + ip_1[0] + ru(-5, 5), + ip[1] - 2, + ip[2] + ru(-0.0, 0.0))).autoretain() + Foothold(( + ip_2[0] + ru(-5, 5), + ip[1] - 2, + ip[2] + ru(-0.0, 0.0))).autoretain() + else: + Foothold(( + ip[0] + ru(-8, 8), + ip[1] - 2, + ip[2]), moving=True).autoretain() + + def teams_or_ffa(self) -> None: + if isinstance(self.session, bs.DualTeamSession): + return 'teams' + return 'ffa' + + def spawn_player_spaz(self, + player: Player, + position: Sequence[float] = (0, 0, 0), + angle: float | None = None) -> PlayerSpaz: + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from babase import _math + from bascenev1._gameutils import animate + + position = self.init_position + if self.teams_or_ffa() == 'teams': + position = self.team_init_positions[player.team.id % 2] + angle = None + + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = _babase.safecolor(color, target_intensity=0.75) + spaz = AbyssPlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player(enable_punch=False, + enable_bomb=True, + enable_pickup=False) + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + return spaz + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = bs.time() + + # Record the player's moment of death. + # assert isinstance(msg.spaz.player + msg.getplayer(Player).death_time = curtime + + # In co-op mode, end the game the instant everyone dies + # (more accurate looking). + # In teams/ffa, allow a one-second fudge-factor so we can + # get more draws if players die basically at the same time. + if isinstance(self.session, bs.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + babase.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = curtime + else: + bs.timer(1.0, self._check_end_game) + + else: + # Default handler: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + # In co-op, we go till everyone is dead.. otherwise we go + # until one team remains. + if isinstance(self.session, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 0: + self.end_game() + + def end_game(self) -> None: + cur_time = bs.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + # Mark death-time as now for any still-living players + # and award players points for how long they lasted. + # (these per-player scores are only meaningful in team-games) + for team in self.teams: + for player in team.players: + survived = False + if player.notIn: + player.death_time = 0 + + # Throw an extra fudge factor in so teams that + # didn't die come out ahead of teams that did. + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + # Award a per-player score depending on how many seconds + # they lasted (per-player scores only affect teams mode; + # everywhere else just looks at the per-team score). + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 # A bit extra for survivors. + self.stats.player_scored(player, score, screenmessage=False) + + # Stop updating our time text, and set the final time to match + # exactly when our last guy died. + self._timer.stop(endtime=self._last_player_death_time) + + # Ok now calc game results: set a score for each team and then tell + # the game to end. + results = bs.GameResults() + + # Remember that 'free-for-all' mode is simply a special form + # of 'teams' mode where each player gets their own team, so we can + # just always deal in teams and have all cases covered. + for team in self.teams: + + # Set the team score to the max time survived by any player on + # that team. + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, + player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) diff --git a/plugins/minigames/drone_war.py b/plugins/minigames/drone_war.py new file mode 100644 index 000000000..8204633f5 --- /dev/null +++ b/plugins/minigames/drone_war.py @@ -0,0 +1,446 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# +""" +DroneWar - Attack enemies with drone, Fly with drone and fire rocket launcher. +Author: Mr.Smoothy +Discord: https://discord.gg/ucyaesh +Youtube: https://www.youtube.com/c/HeySmoothy +Website: https://bombsquad-community.web.app +Github: https://github.com/bombsquad-community +""" +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from babase._mgen.enums import InputType +from bascenev1lib.actor.bomb import Blast + +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.playerspaz import PlayerSpaz, PlayerT +from bascenev1lib.game.deathmatch import DeathMatchGame, Player +from bascenev1lib.actor import spaz +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + +STORAGE_ATTR_NAME = f'_shared_{__name__}_factory' + +# SMoothy's Drone (fixed version of floater) with rocket launcher +# use drone as long as you want , unlike floater which dies after being idle. + + +class Drone(bs.Actor): + def __init__(self, spaz): + super().__init__() + shared = SharedObjects.get() + self._drone_material = bs.Material() + self.loop_ascend = None + self.loop_descend = None + self.loop_lr = None + self.loop_ud = None + self.rocket_launcher = None + self.x_direction = 0 + self.z_direction = 0 + self.spaz = spaz + self._drone_material.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=(('modify_node_collision', 'collide', True), + ('modify_part_collision', 'physical', True))) + self._drone_material.add_actions( + conditions=(('they_have_material', + shared.object_material), 'or', + ('they_have_material', + shared.footing_material), 'or', + ('they_have_material', + self._drone_material)), + actions=('modify_part_collision', 'physical', False)) + self.node = bs.newnode( + 'prop', + delegate=self, + owner=None, + attrs={ + 'position': spaz.node.position, + 'mesh': bs.getmesh('landMine'), + 'light_mesh': bs.getmesh('landMine'), + 'body': 'landMine', + 'body_scale': 1, + 'mesh_scale': 1, + 'shadow_size': 0.25, + 'density': 999999, + 'gravity_scale': 0.0, + 'color_texture': bs.gettexture('achievementCrossHair'), + 'reflection': 'soft', + 'reflection_scale': [0.25], + 'materials': [shared.footing_material, self._drone_material] + }) + self.grab_node = bs.newnode( + 'prop', + owner=self.node, + attrs={ + 'position': (0, 0, 0), + 'body': 'sphere', + 'mesh': None, + 'color_texture': None, + 'body_scale': 0.2, + 'reflection': 'powerup', + 'density': 999999, + 'reflection_scale': [1.0], + 'mesh_scale': 0.2, + 'gravity_scale': 0, + 'shadow_size': 0.1, + 'is_area_of_interest': True, + 'materials': [shared.object_material, self._drone_material] + }) + self.node.connectattr('position', self.grab_node, 'position') + self._rcombine = bs.newnode('combine', + owner=self.node, + attrs={ + 'input0': self.spaz.node.position[0], + 'input1': self.spaz.node.position[1]+3, + 'input2': self.spaz.node.position[2], + 'size': 3 + }) + + self._rcombine.connectattr('output', self.node, 'position') + + def set_rocket_launcher(self, launcher: RocketLauncher): + self.rocket_launcher = launcher + + def fire(self): + if hasattr(self.grab_node, "position"): + self.rocket_launcher.shot(self.spaz, self.x_direction, self.z_direction, ( + self.grab_node.position[0], self.grab_node.position[1] - 1, self.grab_node.position[2])) + + def ascend(self): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input1', { + 0: self.node.position[1], + 1: self.node.position[1] + 2 + }) + loop() + self.loop_ascend = bs.Timer(1, loop, repeat=True) + + def pause_movement(self): + self.loop_ascend = None + + def decend(self): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input1', { + 0: self.node.position[1], + 1: self.node.position[1] - 2 + }) + loop() + self.loop_ascend = bs.Timer(1, loop, repeat=True) + + def pause_lr(self): + self.loop_lr = None + + def pause_ud(self): + self.loop_ud = None + + def left_(self, value=-1): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input0', { + 0: self.node.position[0], + 1: self.node.position[0] + 2 * value + }) + if value == 0.0: + self.loop_lr = None + else: + self.x_direction = value + self.z_direction = 0 + loop() + self.loop_lr = bs.Timer(1, loop, repeat=True) + + def right_(self, value=1): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input0', { + 0: self.node.position[0], + 1: self.node.position[0] + 2 * value + }) + if value == 0.0: + self.loop_lr = None + else: + self.x_direction = value + self.z_direction = 0 + loop() + self.loop_lr = bs.Timer(1, loop, repeat=True) + + def up_(self, value=1): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input2', { + 0: self.node.position[2], + 1: self.node.position[2] - 2 * value + }) + if value == 0.0: + self.loop_ud = None + else: + self.x_direction = 0 + self.z_direction = - value + loop() + self.loop_ud = bs.Timer(1, loop, repeat=True) + + def down_(self, value=-1): + def loop(): + if self.node.exists(): + bs.animate(self._rcombine, 'input2', { + 0: self.node.position[2], + 1: self.node.position[2] - 2 * value + }) + if value == 0.0: + self.loop_ud = None + else: + self.x_direction = 0 + self.z_direction = - value + loop() + self.loop_ud = bs.Timer(1, loop, repeat=True) + + def handlemessage(self, msg): + if isinstance(msg, bs.DieMessage): + self.node.delete() + self.grab_node.delete() + self.loop_ascend = None + self.loop_ud = None + self.loop_lr = None + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + else: + super().handlemessage(msg) + +# =============================================Copied from Quake Game - Dliwk ===================================================================== + + +class RocketFactory: + """Quake Rocket factory""" + + def __init__(self) -> None: + self.ball_material = bs.Material() + + self.ball_material.add_actions( + conditions=((('we_are_younger_than', 5), 'or', + ('they_are_younger_than', 5)), 'and', + ('they_have_material', + SharedObjects.get().object_material)), + actions=('modify_node_collision', 'collide', False)) + + self.ball_material.add_actions( + conditions=('they_have_material', + SharedObjects.get().pickup_material), + actions=('modify_part_collision', 'use_node_collide', False)) + + self.ball_material.add_actions(actions=('modify_part_collision', + 'friction', 0)) + + self.ball_material.add_actions( + conditions=(('they_have_material', + SharedObjects.get().footing_material), 'or', + ('they_have_material', + SharedObjects.get().object_material)), + actions=('message', 'our_node', 'at_connect', ImpactMessage())) + + @classmethod + def get(cls): + """Get factory if exists else create new""" + activity = bs.getactivity() + if hasattr(activity, STORAGE_ATTR_NAME): + return getattr(activity, STORAGE_ATTR_NAME) + factory = cls() + setattr(activity, STORAGE_ATTR_NAME, factory) + return factory + + +class RocketLauncher: + """Very dangerous weapon""" + + def __init__(self): + self.last_shot = bs.time() + + def give(self, spaz: spaz.Spaz) -> None: + """Give spaz a rocket launcher""" + spaz.punch_callback = self.shot + self.last_shot = bs.time() + + # FIXME + # noinspection PyUnresolvedReferences + def shot(self, spaz, x, z, position) -> None: + """Release a rocket""" + time = bs.time() + if time - self.last_shot > 0.6: + self.last_shot = time + + direction = [x, 0, z] + direction[1] = 0.0 + + mag = 10.0 / \ + 1 if babase.Vec3(*direction).length() == 0 else babase.Vec3(*direction).length() + vel = [v * mag for v in direction] + Rocket(position=position, + velocity=vel, + owner=spaz.getplayer(bs.Player), + source_player=spaz.getplayer(bs.Player), + color=spaz.node.color).autoretain() + + +class ImpactMessage: + """Rocket touched something""" + + +class Rocket(bs.Actor): + """Epic rocket from rocket launcher""" + + def __init__(self, + position=(0, 5, 0), + velocity=(1, 0, 0), + source_player=None, + owner=None, + color=(1.0, 0.2, 0.2)) -> None: + super().__init__() + self.source_player = source_player + self.owner = owner + self._color = color + factory = RocketFactory.get() + + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'mesh': bs.getmesh('impactBomb'), + 'body': 'sphere', + 'color_texture': bs.gettexture( + 'bunnyColor'), + 'mesh_scale': 0.2, + 'is_area_of_interest': True, + 'body_scale': 0.8, + 'materials': [ + SharedObjects.get().object_material, + factory.ball_material] + }) # yapf: disable + self.node.extra_acceleration = (self.node.velocity[0] * 200, 0, + self.node.velocity[2] * 200) + + self._life_timer = bs.Timer( + 5, bs.WeakCall(self.handlemessage, bs.DieMessage())) + + self._emit_timer = bs.Timer(0.001, bs.WeakCall(self.emit), repeat=True) + self.base_pos_y = self.node.position[1] + + bs.camerashake(5.0) + + def emit(self) -> None: + """Emit a trace after rocket""" + bs.emitfx(position=self.node.position, + scale=0.4, + spread=0.01, + chunk_type='spark') + if not self.node: + return + self.node.position = (self.node.position[0], self.base_pos_y, + self.node.position[2]) # ignore y + bs.newnode('explosion', + owner=self.node, + attrs={ + 'position': self.node.position, + 'radius': 0.2, + 'color': self._color + }) + + def handlemessage(self, msg: Any) -> Any: + """Message handling for rocket""" + super().handlemessage(msg) + if isinstance(msg, ImpactMessage): + self.node.handlemessage(bs.DieMessage()) + + elif isinstance(msg, bs.DieMessage): + if self.node: + Blast(position=self.node.position, + blast_radius=2, + source_player=self.source_player) + + self.node.delete() + self._emit_timer = None + + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + + +# ba_meta export bascenev1.GameActivity +class ChooseQueen(DeathMatchGame): + name = 'Drone War' + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium'] + + def spawn_player_spaz( + self, + player: PlayerT, + position: Sequence[float] | None = None, + angle: float | None = None, + ) -> PlayerSpaz: + spaz = super().spawn_player_spaz(player, position, angle) + self.spawn_drone(spaz) + return spaz + + def on_begin(self): + super().on_begin() + shared = SharedObjects.get() + self.ground_material = bs.Material() + self.ground_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('call', 'at_connect', babase.Call(self._handle_player_collide)), + ), + ) + pos = (0, 0.1, -5) + self.main_region = bs.newnode('region', attrs={'position': pos, 'scale': ( + 30, 0.001, 23), 'type': 'box', 'materials': [shared.footing_material, self.ground_material]}) + + def _handle_player_collide(self): + try: + player = bs.getcollision().opposingnode.getdelegate( + PlayerSpaz, True) + except bs.NotFoundError: + return + if player.is_alive(): + player.shatter(True) + + def spawn_drone(self, spaz): + with bs.get_foreground_host_activity().context: + + drone = Drone(spaz) + r_launcher = RocketLauncher() + drone.set_rocket_launcher(r_launcher) + player = spaz.getplayer(Player, True) + spaz.node.hold_node = drone.grab_node + player.actor.disconnect_controls_from_player() + player.resetinput() + player.assigninput(InputType.PICK_UP_PRESS, drone.ascend) + player.assigninput(InputType.PICK_UP_RELEASE, drone.pause_movement) + player.assigninput(InputType.JUMP_PRESS, drone.decend) + player.assigninput(InputType.JUMP_RELEASE, drone.pause_movement) + player.assigninput(InputType.PUNCH_PRESS, drone.fire) + player.assigninput(InputType.LEFT_PRESS, drone.left_) + player.assigninput(InputType.RIGHT_PRESS, drone.right_) + player.assigninput(InputType.LEFT_RELEASE, drone.pause_lr) + player.assigninput(InputType.RIGHT_RELEASE, drone.pause_lr) + player.assigninput(InputType.UP_PRESS, drone.up_) + player.assigninput(InputType.DOWN_PRESS, drone.down_) + player.assigninput(InputType.UP_RELEASE, drone.pause_ud) + player.assigninput(InputType.DOWN_RELEASE, drone.pause_ud) diff --git a/plugins/minigames/egg_game.py b/plugins/minigames/egg_game.py new file mode 100644 index 000000000..30bdcf263 --- /dev/null +++ b/plugins/minigames/egg_game.py @@ -0,0 +1,486 @@ +# Tool used to make porting easier.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. + +"""Egg game and support classes.""" +# The Egg Game - throw egg as far as you can +# created in BCS (Bombsquad Consultancy Service) - opensource bombsquad mods for all +# discord.gg/ucyaesh join now and give your contribution +# The Egg game by mr.smoothy +# ba_meta require api 9 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.flag import Flag +import math +import random +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + + +class Puck(bs.Actor): + """A lovely giant hockey puck.""" + + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch = None + self.scored = False + self.egg_mesh = bs.getmesh('egg') + self.egg_tex_1 = bs.gettexture('eggTex1') + self.egg_tex_2 = bs.gettexture('eggTex2') + self.egg_tex_3 = bs.gettexture('eggTex3') + self.eggtx = [self.egg_tex_1, self.egg_tex_2, self.egg_tex_3] + regg = random.randrange(0, 3) + assert activity is not None + assert isinstance(activity, EggGame) + pmats = [shared.object_material, activity.puck_material] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': self.egg_mesh, + 'color_texture': self.eggtx[regg], + 'body': 'capsule', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.5, + 'body_scale': 0.7, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 0.7, 0.26: 0.6}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch = s_player + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class EggGame(bs.TeamGameActivity[Player, Team]): + """Egg game.""" + + name = 'Epic Egg Game' + description = 'Score some goals.' + available_settings = [ + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('40 Seconds', 40), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.1), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('football') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self.slow_motion = True + self._scoreboard = Scoreboard() + self._cheer_sound = bui.getsound('cheer') + self._chant_sound = bui.getsound('crowdChant') + self._foghorn_sound = bui.getsound('foghorn') + self._swipsound = bui.getsound('swip') + self._whistle_sound = bui.getsound('refWhistle') + self.puck_mesh = bs.getmesh('bomb') + self.puck_tex = bs.gettexture('landMine') + self.puck_scored_tex = bs.gettexture('landMineLit') + self._puck_sound = bui.getsound('metalHit') + self.puck_material = bs.Material() + self._fake_wall_material = bs.Material() + self.HIGHEST = 0 + self._fake_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + # self.puck_material.add_actions(conditions=('they_have_material', + # shared.footing_material), + # actions=('impact_sound', + # self._puck_sound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', bs.DieMessage()))) + # self.puck_material.add_actions( + # conditions=('they_have_material',shared.footing_material) + # actions=(('modify_part_collision', 'collide', + # True), ('modify_part_collision', 'physical', True), + # ('call', 'at_connect', self._handle_egg_collision)) + # ) + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self.main_ground_material = bs.Material() + + self.main_ground_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_egg_collision))) + + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[bs.NodeActor]] = None + self._puck: Optional[Puck] = None + self._pucks = [] + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + return "Throw Egg as far u can" + + def get_instance_description_short(self) -> Union[str, Sequence]: + return "Throw Egg as far u can" + + def on_begin(self) -> None: + super().on_begin() + if self._time_limit == 0.0: + self._time_limit = 60 + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + self._puck_spawn_pos = self.map.get_flag_position(None) + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + pos = (11.88630542755127, 0.3009839951992035, 1.33331298828125) + # mat=bs.Material() + # mat.add_actions( + + # actions=( ('modify_part_collision','physical',True), + # ('modify_part_collision','collide',True)) + # ) + # self._score_regions.append( + # bs.NodeActor( + # bs.newnode('region', + # attrs={ + # 'position': pos, + # 'scale': (2,3,5), + # 'type': 'box', + # 'materials': [self._score_region_material] + # }))) + # pos=(-11.88630542755127, 0.3009839951992035, 1.33331298828125) + # self._score_regions.append( + # bs.NodeActor( + # bs.newnode('region', + # attrs={ + # 'position': pos, + # 'scale': (2,3,5), + # 'type': 'box', + # 'materials': [self._score_region_material] + # }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (-9.21, defs.boxes['goal2'][0:3][1], defs.boxes['goal2'][0:3][2]), + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': (self._fake_wall_material, ) + }))) + pos = (0, 0.1, -5) + self.main_ground = bs.newnode('region', attrs={'position': pos, 'scale': ( + 25, 0.001, 22), 'type': 'box', 'materials': [self.main_ground_material]}) + self._update_scoreboard() + self._chant_sound.play() + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = bs.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + puck.last_players_to_touch = player + + def _kill_puck(self) -> None: + self._puck = None + + def _handle_egg_collision(self) -> None: + + no = bs.getcollision().opposingnode + pos = no.position + egg = no.getdelegate(Puck) + source_player = egg.last_players_to_touch + if source_player == None or pos[0] < -8 or not source_player.node.exists(): + return + + try: + col = source_player.team.color + self.flagg = Flag(touchable=False, position=pos, color=col).autoretain() + self.flagg.is_area_of_interest = True + player_pos = source_player.node.position + + distance = math.sqrt(pow(player_pos[0]-pos[0], 2) + pow(player_pos[2]-pos[2], 2)) + + dis_mark = bs.newnode('text', + + attrs={ + 'text': str(round(distance, 2))+"m", + 'in_world': True, + 'scale': 0.02, + 'h_align': 'center', + 'position': (pos[0], 1.6, pos[2]), + 'color': col + }) + bs.animate(dis_mark, 'scale', { + 0.0: 0, 0.5: 0.01 + }) + if distance > self.HIGHEST: + self.HIGHEST = distance + self.stats.player_scored( + source_player, + 10, + big_message=False) + + no.delete() + bs.timer(2, self._spawn_puck) + source_player.team.score = int(distance) + + except (): + pass + + def spawn_player(self, player: Player) -> bs.Actor: + + zoo = random.randrange(-4, 5) + pos = (-11.204887390136719, 0.2998693287372589, zoo) + spaz = self.spawn_player_spaz( + player, position=pos, angle=90) + assert spaz.node + + # Prevent controlling of characters before the start of the race. + + return spaz + + def _handle_score(self) -> None: + """A point has been scored.""" + + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = bs.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 20, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + self._foghorn_sound.play() + self._cheer_sound.play() + + # self._puck.scored = True + + # Change puck texture to something cool + # self._puck.node.color_texture = self.puck_scored_tex + # Kill the puck (it'll respawn itself shortly). + bs.timer(1.0, self._kill_puck) + + # light = bs.newnode('light', + # attrs={ + # 'position': bs.getcollision().position, + # 'height_attenuated': False, + # 'color': (1, 0, 0) + # }) + # bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + # bs.timer(1.0, light.delete) + + bs.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + # for team in self.teams: + # self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + bs.timer(3.0, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + # light = bs.newnode('light', + # attrs={ + # 'position': self._puck_spawn_pos, + # 'height_attenuated': False, + # 'color': (1, 0, 0) + # }) + # bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + # bs.timer(1.0, light.delete) + pass + + def _spawn_puck(self) -> None: + # self._swipsound.play() + # self._whistle_sound.play() + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + zoo = random.randrange(-5, 6) + pos = (-11.204887390136719, 0.2998693287372589, zoo) + self._pucks.append(Puck(position=pos)) diff --git a/plugins/minigames/explodo_run.py b/plugins/minigames/explodo_run.py new file mode 100644 index 000000000..6d41f4dc6 --- /dev/null +++ b/plugins/minigames/explodo_run.py @@ -0,0 +1,143 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazbot import SpazBotSet, ExplodeyBot, SpazBotDiedMessage +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Type, Dict, List, Optional + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + +# ba_meta export bascenev1.GameActivity + + +class ExplodoRunGame(bs.TeamGameActivity[Player, Team]): + name = "Explodo Run" + description = "Run For Your Life :))" + available_settings = [bs.BoolSetting('Epic Mode', default=False)] + scoreconfig = bs.ScoreConfig(label='Time', + scoretype=bs.ScoreType.MILLISECONDS, + lower_is_better=False) + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'rampagePreview' + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Rampage'] + + def __init__(self, settings: dict): + settings['map'] = "Rampage" + self._epic_mode = settings.get('Epic Mode', False) + if self._epic_mode: + self.slow_motion = True + super().__init__(settings) + self._timer: Optional[OnScreenTimer] = None + self._winsound = bs.getsound('score') + self._won = False + self._bots = SpazBotSet() + self.wave = 1 + self.default_music = bs.MusicType.TO_THE_DEATH + + def on_begin(self) -> None: + super().on_begin() + + self._timer = OnScreenTimer() + bs.timer(2.5, self._timer.start) + + # Bots Hehe + bs.timer(2.5, self.street) + + def street(self): + for a in range(self.wave): + p1 = random.choice([-5, -2.5, 0, 2.5, 5]) + p3 = random.choice([-4.5, -4.14, -5, -3]) + time = random.choice([1, 1.5, 2.5, 2]) + self._bots.spawn_bot(ExplodeyBot, pos=(p1, 5.5, p3), spawn_time=time) + self.wave += 1 + + def botrespawn(self): + if not self._bots.have_living_bots(): + self.street() + + def handlemessage(self, msg: Any) -> Any: + + # A player has died. + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard behavior. + self._won = True + self.end_game() + + # A spaz-bot has died. + elif isinstance(msg, SpazBotDiedMessage): + # Unfortunately the bot-set will always tell us there are living + # bots if we ask here (the currently-dying bot isn't officially + # marked dead yet) ..so lets push a call into the event loop to + # check once this guy has finished dying. + babase.pushcall(self.botrespawn) + + # Let the base class handle anything we don't. + else: + return super().handlemessage(msg) + return None + + # When this is called, we should fill out results and end the game + # *regardless* of whether is has been won. (this may be called due + # to a tournament ending or other external reason). + def end_game(self) -> None: + + # Stop our on-screen timer so players can see what they got. + assert self._timer is not None + self._timer.stop() + + results = bs.GameResults() + + # If we won, set our score to the elapsed time in milliseconds. + # (there should just be 1 team here since this is co-op). + # ..if we didn't win, leave scores as default (None) which means + # we lost. + if self._won: + elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0) + bs.cameraflash() + self._winsound.play() + for team in self.teams: + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage()) + results.set_team_score(team, elapsed_time_ms) + + # Ends the activity. + self.end(results) + + +# ba_meta export plugin +class plugin(babase.Plugin): + def __init__(self): + ## Campaign support ## + babase.app.classic.add_coop_practice_level(bs.Level( + name='Explodo Run', + gametype=ExplodoRunGame, + settings={}, + preview_texture_name='rampagePreview')) + babase.app.classic.add_coop_practice_level(bs.Level('Epic Explodo Run', + gametype=ExplodoRunGame, + settings={'Epic Mode': True}, + preview_texture_name='rampagePreview')) diff --git a/plugins/minigames/extinction.py b/plugins/minigames/extinction.py new file mode 100644 index 000000000..6f671cf40 --- /dev/null +++ b/plugins/minigames/extinction.py @@ -0,0 +1,255 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +"""For 1.7.33""" + +# ba_meta require api 8 + +from __future__ import annotations +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor.bomb import BombFactory, Blast, ImpactMessage +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, Type + + +class Meteor(bs.Actor): + """A giant meteor instead of bombs.""" + + def __init__(self, + pos: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0)): + super().__init__() + + shared = SharedObjects.get() + factory = BombFactory.get() + + materials = (shared.object_material, + factory.impact_blast_material) + + self.pos = (pos[0], pos[1], pos[2]) + self.velocity = (velocity[0], velocity[1], velocity[2]) + + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'position': self.pos, + 'velocity': self.velocity, + 'mesh': factory.sticky_bomb_mesh, + 'color_texture': factory.tnt_tex, + 'mesh_scale': 3.0, + 'body_scale': 2.99, + 'body': 'sphere', + 'shadow_size': 0.5, + 'reflection': 'soft', + 'reflection_scale': [0.45], + 'materials': materials + }) + + def explode(self) -> None: + Blast(position=self.node.position, + velocity=self.node.velocity, + blast_type='tnt', + blast_radius=2.0) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + elif isinstance(msg, ImpactMessage): + self.explode() + self.handlemessage(bs.DieMessage()) + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self): + super().__init__() + self.death_time: Optional[float] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class NewMeteorShowerGame(bs.TeamGameActivity[Player, Team]): + """Minigame by Jetz.""" + + name = 'Extinction' + description = 'Survive the Extinction.' + available_settings = [ + bs.BoolSetting('Epic Mode', default=False)] + + announce_player_deaths = True + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium'] + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.FreeForAllSession) + or issubclass(sessiontype, bs.DualTeamSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + + self._epic_mode = bool(settings['Epic Mode']) + self._last_player_death_time: Optiobal[float] = None + self._meteor_time = 2.0 + self._timer: Optional[OnScreenTimer] = None + + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + + if self._epic_mode: + self.slow_motion = True + + def on_begin(self) -> None: + super().on_begin() + + delay = 5.0 if len(self.players) > 2 else 2.5 + if self._epic_mode: + delay *= 0.25 + bs.timer(delay, self._decrement_meteor_time, repeat=True) + + delay = 3.0 + if self._epic_mode: + delay *= 0.25 + bs.timer(delay, self._set_meteor_timer) + + self._timer = OnScreenTimer() + self._timer.start() + self._check_end_game() + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + assert self._timer is not None + player.death_time = self._timer.getstarttime() + return + self.spawn_player(player) + + def spawn_player(self, player: Player) -> None: + spaz = self.spawn_player_spaz(player) + + spaz.connect_controls_to_player(enable_punch=False, + enable_pickup=False, + enable_bomb=False, + enable_jump=False) + spaz.play_big_death_sound = True + + return spaz + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + + self._check_end_game() + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + curtime = bs.time() + + msg.getplayer(Player).death_time = curtime + bs.timer(1.0, self._check_end_game) + else: + return super().handlemessage(msg) + + def _spawn_meteors(self) -> None: + pos = (random.randint(-6, 7), 12, + random.uniform(-2, 1)) + velocity = (random.randint(-11, 11), 0, + random.uniform(-5, 5)) + Meteor(pos=pos, velocity=velocity).autoretain() + + def _spawn_meteors_cluster(self) -> None: + delay = 0.0 + for _i in range(random.randrange(1, 3)): + bs.timer(delay, self._spawn_meteors) + delay += 1 + self._set_meteor_timer() + + def _decrement_meteor_time(self) -> None: + self._meteor_time = max(0.01, self._meteor_time * 0.9) + + def _set_meteor_timer(self) -> None: + bs.timer((1.0 + 0.2 * random.random()) * self._meteor_time, + self._spawn_meteors_cluster) + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + if isinstance(self.session, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def end_game(self) -> None: + cur_time = bs.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + for team in self.teams: + for player in team.players: + survived = False + + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 + self.stats.player_scored(player, score, screenmessage=False) + + self._timer.stop(endtime=self._last_player_death_time) + + results = bs.GameResults() + + for team in self.teams: + + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, + player.death_time - start_time) + + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) + + +# ba_meta export plugin +class plugin(babase.Plugin): + def __init__(self): + ## Campaign support ## + babase.app.classic.add_coop_practice_level(bs.Level( + name='Extinction', + gametype=NewMeteorShowerGame, + settings={'Epic Mode': False}, + preview_texture_name='footballStadiumPreview')) + babase.app.classic.add_coop_practice_level(bs.Level('Epic Extinction', + gametype=NewMeteorShowerGame, + settings={'Epic Mode': True}, + preview_texture_name='footballStadiumPreview')) diff --git a/plugins/minigames/fat_pigs.py b/plugins/minigames/fat_pigs.py new file mode 100644 index 000000000..78c56c37a --- /dev/null +++ b/plugins/minigames/fat_pigs.py @@ -0,0 +1,339 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 + +# - - - - - - - - - - - - - - - - - - - - - +# - Fat-Pigs! by Zacker Tz || Zacker#5505 - +# - Version 0.01 :v - +# - - - - - - - - - - - - - - - - - - - - - + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import random +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Union, Sequence, Optional + +# - - - - - - - Mini - Settings - - - - - - - - - - - - - - - - # + +zkBombs_limit = 3 # Number of bombs you can use | Default = 3 +zkPunch = False # Enable/Disable punchs | Default = False +zkPickup = False # Enable/Disable pickup | Default = False + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + +# ba_meta export bascenev1.GameActivity + + +class FatPigs(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Fat-Pigs!' + description = 'Survive...' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=0.25, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Courtyard', 'Rampage', 'Monkey Face', 'Lake Frigid', 'Step Right Up'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._meteor_time = 2.0 + self._score_to_win: Optional[int] = None + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + # self._text_credit = bool(settings['Credits']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Crush ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + # Ambiente + gnode = bs.getactivity().globalsnode + gnode.tint = (0.8, 1.2, 0.8) + gnode.ambient_color = (0.7, 1.0, 0.6) + gnode.vignette_outer = (0.4, 0.6, 0.4) # C + # gnode.vignette_inner = (0.9, 0.9, 0.9) + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + delay = 5.0 if len(self.players) > 2 else 2.5 + if self._epic_mode: + delay *= 0.25 + bs.timer(delay, self._decrement_meteor_time, repeat=False) + + # Kick off the first wave in a few seconds. + delay = 3.0 + if self._epic_mode: + delay *= 0.25 + bs.timer(delay, self._set_meteor_timer) + + # self._timer = OnScreenTimer() + # self._timer.start() + + # Check for immediate end (if we've only got 1 player, etc). + bs.timer(5.0, self._check_end_game) + + t = bs.newnode('text', + attrs={'text': "Minigame by Zacker Tz", + 'scale': 0.7, + 'position': (0.001, 625), + 'shadow': 0.5, + 'opacity': 0.7, + 'flatness': 1.2, + 'color': (0.6, 1, 0.6), + 'h_align': 'center', + 'v_attach': 'bottom'}) + + def spawn_player(self, player: Player) -> bs.Actor: + spaz = self.spawn_player_spaz(player) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player(enable_punch=zkPunch, + enable_bomb=True, + enable_pickup=zkPickup) + + spaz.bomb_count = zkBombs_limit + spaz._max_bomb_count = zkBombs_limit + spaz.bomb_type_default = 'sticky' + spaz.bomb_type = 'sticky' + + # cerdo gordo + spaz.node.color_mask_texture = bs.gettexture('melColorMask') + spaz.node.color_texture = bs.gettexture('melColor') + spaz.node.head_mesh = bs.getmesh('melHead') + spaz.node.hand_mesh = bs.getmesh('melHand') + spaz.node.torso_mesh = bs.getmesh('melTorso') + spaz.node.pelvis_mesh = bs.getmesh('kronkPelvis') + spaz.node.upper_arm_mesh = bs.getmesh('melUpperArm') + spaz.node.forearm_mesh = bs.getmesh('melForeArm') + spaz.node.upper_leg_mesh = bs.getmesh('melUpperLeg') + spaz.node.lower_leg_mesh = bs.getmesh('melLowerLeg') + spaz.node.toes_mesh = bs.getmesh('melToes') + spaz.node.style = 'mel' + # Sounds cerdo gordo + mel_sounds = [bs.getsound('mel01'), bs.getsound('mel02'), bs.getsound('mel03'), bs.getsound('mel04'), bs.getsound('mel05'), + bs.getsound('mel06'), bs.getsound('mel07'), bs.getsound('mel08'), bs.getsound('mel09'), bs.getsound('mel10')] + spaz.node.jump_sounds = mel_sounds + spaz.node.attack_sounds = mel_sounds + spaz.node.impact_sounds = mel_sounds + spaz.node.pickup_sounds = mel_sounds + spaz.node.death_sounds = [bs.getsound('melDeath01')] + spaz.node.fall_sounds = [bs.getsound('melFall01')] + + def _set_meteor_timer(self) -> None: + bs.timer((1.0 + 0.2 * random.random()) * self._meteor_time, + self._drop_bomb_cluster) + + def _drop_bomb_cluster(self) -> None: + + # Random note: code like this is a handy way to plot out extents + # and debug things. + loc_test = False + if loc_test: + bs.newnode('locator', attrs={'position': (8, 6, -5.5)}) + bs.newnode('locator', attrs={'position': (8, 6, -2.3)}) + bs.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) + bs.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) + + # Drop several bombs in series. + delay = 0.0 + for _i in range(random.randrange(1, 3)): + # Drop them somewhere within our bounds with velocity pointing + # toward the opposite side. + pos = (-7.3 + 15.3 * random.random(), 11, + -5.5 + 2.1 * random.random()) + dropdir = (-1.0 if pos[0] > 0 else 1.0) + vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0) + bs.timer(delay, babase.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + def _drop_bomb(self, position: Sequence[float], + velocity: Sequence[float]) -> None: + Bomb(position=position, velocity=velocity, bomb_type='sticky').autoretain() + + def _decrement_meteor_time(self) -> None: + self._meteor_time = max(0.01, self._meteor_time * 0.9) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + self._dingsound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + self._dingsound.play() + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + # In co-op, we go till everyone is dead.. otherwise we go + # until one team remains. + if isinstance(self.session, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/plugins/minigames/flag_day.py b/plugins/minigames/flag_day.py new file mode 100644 index 000000000..534ec6f22 --- /dev/null +++ b/plugins/minigames/flag_day.py @@ -0,0 +1,615 @@ + +# Ported by brostos to api 8 +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import json +import math +import random +from bascenev1lib.game.elimination import Icon +from bascenev1lib.actor.bomb import Bomb, Blast +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBox +from bascenev1lib.actor.flag import Flag, FlagPickedUpMessage +from bascenev1lib.actor.spazbot import SpazBotSet, BrawlerBotLite, SpazBotDiedMessage + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = bs.app.lang.language +if lang == 'Spanish': + name = 'Día de la Bandera' + description = ('Recoge las banderas para recibir un premio.\n' + 'Pero ten cuidado...') + slow_motion_deaths = 'Muertes en Cámara Lenta' + credits = 'Creado por MattZ45986 en Github | Actualizado por byANG3L and brostos' + you_were = 'Estas' + cursed_text = 'MALDITO' + run = 'CORRE' + climb_top = 'Escala a la cima' + bomb_rain = '¡LLUVIA DE BOMBAS!' + lame_guys = 'Chicos Ligeros' + jackpot = '¡PREMIO MAYOR!' + diedtxt = '¡' + diedtxt2 = ' ha sido eliminado!' +else: + name = 'Flag Day' + description = 'Pick up flags to receive a prize.\nBut beware...' + slow_motion_deaths = 'Slow Motion Deaths' + credits = 'Created by MattZ45986 on Github | Updated by byANG3L and brostos' + you_were = 'You were' + cursed_text = 'CURSED' + run = 'RUN' + climb_top = 'Climb to the top' + bomb_rain = 'BOMB RAIN!' + lame_guys = 'Lame Guys' + jackpot = '!JACKPOT!' + diedtxt = '' + diedtxt2 = ' died!' + + +class Icon(Icon): + + def __init__( + self, + player: Player, + position: tuple[float, float], + scale: float, + show_lives: bool = True, + show_death: bool = True, + name_scale: float = 1.0, + name_maxwidth: float = 115.0, + flatness: float = 1.0, + shadow: float = 1.0, + dead: bool = False, + ): + super().__init__(player, position, scale, show_lives, show_death, + name_scale, name_maxwidth, flatness, shadow) + if dead: + self._name_text.opacity = 0.2 + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + +class FlagBearer(PlayerSpaz): + def handlemessage(self, msg: Any) -> Any: + super().handlemessage(msg) + if isinstance(msg, bs.PowerupMessage): + activity = self.activity + player = self.getplayer(Player) + if not player.is_alive(): + return + if activity.last_prize == 'curse': + player.team.score += 25 + activity._update_scoreboard() + elif activity.last_prize == 'land_mines': + player.team.score += 15 + activity._update_scoreboard() + self.connect_controls_to_player() + elif activity.last_prize == 'climb': + player.team.score += 50 + activity._update_scoreboard() + if msg.poweruptype == 'health': + activity.round_timer = None + bs.timer(0.2, activity.setup_next_round) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.dead = False + self.icons: list[Icon] = [] + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class FlagDayGame(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = name + description = description + + # Print messages when players die since it matters here. + announce_player_deaths = True + + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[babase.Setting]: + settings = [ + bs.BoolSetting(slow_motion_deaths, default=True), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return ( + issubclass(sessiontype, bs.CoopSession) + or issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession) + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Courtyard'] + + def __init__(self, settings: dict): + super().__init__(settings) + self.credits() + self._scoreboard = Scoreboard() + self._dingsound = bui.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._slow_motion_deaths = bool(settings[slow_motion_deaths]) + self.current_player: Player | None = None + self.prize_recipient: Player | None = None + self.bomb_survivor: Player | None = None + self.bad_guy_cost: int = 0 + self.player_index: int = 0 + self.bombs: list = [] + self.queue_line: list = [] + self._bots: SpazBotSet | None = None + self.light: bs.Node | None = None + self.last_prize = 'none' + self._flag: Flag | None = None + self._flag2: Flag | None = None + self._flag3: Flag | None = None + self._flag4: Flag | None = None + self._flag5: Flag | None = None + self._flag6: Flag | None = None + self._flag7: Flag | None = None + self._flag8: Flag | None = None + self.set = False + self.round_timer: bs.Timer | None = None + self.give_points_timer: bs.Timer | None = None + + self._jackpot_sound = bui.getsound('achievement') + self._round_sound = bui.getsound('powerup01') + self._dingsound = bui.getsound('dingSmall') + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.TO_THE_DEATH + ) + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + if player is self.current_player: + self.setup_next_round() + self._check_end_game() + super().on_player_leave(player) + self.queue_line.remove(player) + self._update_icons() + + def on_begin(self) -> None: + super().on_begin() + for player in self.players: + if player.actor: + player.actor.handlemessage(bs.DieMessage()) + player.actor.node.delete() + self.queue_line.append(player) + self.spawn_player_spaz( + self.queue_line[self.player_index % len(self.queue_line)], + (0.0, 3.0, -2.0)) + self.current_player = self.queue_line[0] + # Declare a set of bots (enemies) that we will use later + self._bots = SpazBotSet() + self.reset_flags() + self._update_icons() + self._update_scoreboard() + + def credits(self) -> None: + bs.newnode( + 'text', + attrs={ + 'v_attach': 'bottom', + 'h_align': 'center', + 'vr_depth': 0, + 'color': (0, 0.2, 0), + 'shadow': 1.0, + 'flatness': 1.0, + 'position': (0, 0), + 'scale': 0.8, + 'text': credits + }) + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + for player in self.queue_line: + player.icons = [] + if player == self.current_player: + xval = 0 + x_offs = -78 + player.icons.append( + Icon(player, + position=(xval, 65), + scale=1.0, + name_maxwidth=130, + name_scale=0.8, + flatness=0.0, + shadow=0.5, + show_death=True, + show_lives=False)) + elif player.dead: + xval = 65 + x_offs = 78 + player.icons.append( + Icon(player, + position=(xval, 50), + scale=0.5, + name_maxwidth=75, + name_scale=1.0, + flatness=1.0, + shadow=1.0, + show_death=False, + show_lives=False, + dead=True)) + xval += x_offs * 0.56 + else: + xval = -65 + x_offs = 78 + player.icons.append( + Icon(player, + position=(xval, 50), + scale=0.5, + name_maxwidth=75, + name_scale=1.0, + flatness=1.0, + shadow=1.0, + show_death=False, + show_lives=False)) + xval -= x_offs * 0.56 + + def give_prize(self, prize: int) -> None: + if prize == 1: + # Curse him aka make him blow up in 5 seconds + # give them a nice message + bs.broadcastmessage(you_were, color=(0.1, 0.1, 0.1)) + bs.broadcastmessage(cursed_text, color=(1.0, 0.0, 0.0)) + self.make_health_box((0.0, 0.0, 0.0)) + self.last_prize = 'curse' + self.prize_recipient.actor.curse() + # bs.timer(5.5, self.setup_next_round) + if prize == 2: + self.setup_rof() + bs.broadcastmessage(run, color=(1.0, 0.2, 0.1)) + self.last_prize = 'ring_of_fire' + if prize == 3: + self.last_prize = 'climb' + self.light = bs.newnode( + 'locator', + attrs={ + 'shape': 'circle', + 'position': (0.0, 3.0, -9.0), + 'color': (1.0, 1.0, 1.0), + 'opacity': 1.0, + 'draw_beauty': True, + 'additive': True + }) + bs.broadcastmessage(climb_top, color=(0.5, 0.5, 0.5)) + bs.timer(3.0, babase.Call(self.make_health_box, (0.0, 6.0, -9.0))) + self.round_timer = bs.Timer(10.0, self.setup_next_round) + if prize == 4: + self.last_prize = 'land_mines' + self.make_health_box((6.0, 5.0, -2.0)) + self.make_land_mines() + self.prize_recipient.actor.connect_controls_to_player( + enable_bomb=False) + self.prize_recipient.actor.node.handlemessage( + bs.StandMessage(position=(-6.0, 3.0, -2.0))) + self.round_timer = bs.Timer(7.0, self.setup_next_round) + if prize == 5: + # Make it rain bombs + self.bomb_survivor = self.prize_recipient + bs.broadcastmessage(bomb_rain, color=(1.0, 0.5, 0.16)) + # Set positions for the bombs to drop + for bzz in range(-5, 6): + for azz in range(-5, 2): + # for each position make a bomb drop there + self.make_bomb(bzz, azz) + self.give_points_timer = bs.Timer(3.3, self.give_points) + self.last_prize = 'bombrain' + if prize == 6: + self.setup_br() + self.bomb_survivor = self.prize_recipient + self.give_points_timer = bs.Timer(7.0, self.give_points) + self.last_prize = 'bombroad' + if prize == 7: + # makes killing a bad guy worth ten points + self.bad_guy_cost = 2 + bs.broadcastmessage(lame_guys, color=(1.0, 0.5, 0.16)) + # makes a set of nine positions + for a in range(-1, 2): + for b in range(-3, 0): + # and spawns one in each position + self._bots.spawn_bot(BrawlerBotLite, pos=(a, 2.5, b)) + # and we give our player boxing gloves and a shield + self._player.equip_boxing_gloves() + self._player.equip_shields() + self.last_prize = 'lameguys' + if prize == 8: + self._jackpot_sound.play() + bs.broadcastmessage(jackpot, color=(1.0, 0.0, 0.0)) + bs.broadcastmessage(jackpot, color=(0.0, 1.0, 0.0)) + bs.broadcastmessage(jackpot, color=(0.0, 0.0, 1.0)) + team = self.prize_recipient.team + # GIVE THEM A WHOPPING 50 POINTS!!! + team.score += 50 + # and update the scores + self._update_scoreboard() + self.last_prize = 'jackpot' + bs.timer(2.0, self.setup_next_round) + + def setup_next_round(self) -> None: + if self._slow_motion_deaths: + bs.getactivity().globalsnode.slow_motion = False + if self.set: + return + if self.light: + self.light.delete() + for bomb in self.bombs: + bomb.handlemessage(bs.DieMessage()) + self.kill_flags() + self._bots.clear() + self.reset_flags() + self.current_player.actor.handlemessage( + bs.DieMessage(how='game')) + self.current_player.actor.node.delete() + c = 0 + self.player_index += 1 + self.player_index %= len(self.queue_line) + if len(self.queue_line) > 0: + while self.queue_line[self.player_index].dead: + if c > len(self.queue_line): + return + self.player_index += 1 + self.player_index %= len(self.queue_line) + c += 1 + self.spawn_player_spaz( + self.queue_line[self.player_index], (0.0, 3.0, -2.0)) + self.current_player = self.queue_line[self.player_index] + self.last_prize = 'none' + self._update_icons() + + def check_bots(self) -> None: + if not self._bots.have_living_bots(): + self.setup_next_round() + + def make_land_mines(self) -> None: + self.bombs = [] + for i in range(-11, 7): + self.bombs.append(Bomb( + position=(0.0, 6.0, i/2.0), + bomb_type='land_mine', + blast_radius=2.0)) + self.bombs[i+10].arm() + + def give_points(self) -> None: + if self.bomb_survivor is not None and self.bomb_survivor.is_alive(): + self.bomb_survivor.team.score += 20 + self._update_scoreboard() + self.round_timer = bs.Timer(1.0, self.setup_next_round) + + def make_health_box(self, position: Sequence[float]) -> None: + if position == (0.0, 3.0, 0.0): + position = (random.randint(-6, 6), 6, random.randint(-6, 4)) + elif position == (0, 0, 0): + position = random.choice( + ((-7, 6, -5), (7, 6, -5), (-7, 6, 1), (7, 6, 1))) + self.health_box = PowerupBox( + position=position, poweruptype='health').autoretain() + + # called in prize #5 + def make_bomb(self, xpos: float, zpos: float) -> None: + # makes a bomb at the given position then auto-retains it aka: + # makes sure it doesn't disappear because there is no reference to it + self.bombs.append(Bomb(position=(xpos, 12, zpos))) + + def setup_br(self) -> None: + self.make_bomb_row(6) + self.prize_recipient.actor.handlemessage( + bs.StandMessage(position=(6.0, 3.0, -2.0))) + + def make_bomb_row(self, num: int) -> None: + if not self.prize_recipient.is_alive(): + return + if num == 0: + self.round_timer = bs.Timer(1.0, self.setup_next_round) + return + for i in range(-11, 7): + self.bombs.append( + Bomb(position=(-3, 3, i/2.0), + velocity=(12, 0.0, 0.0), + bomb_type='normal', + blast_radius=1.2)) + bs.timer(1.0, babase.Call(self.make_bomb_row, num-1)) + + def setup_rof(self) -> None: + self.make_blast_ring(10) + self.prize_recipient.actor.handlemessage( + bs.StandMessage(position=(0.0, 3.0, -2.0))) + + def make_blast_ring(self, length: float) -> None: + if not self.prize_recipient.is_alive(): + return + if length == 0: + self.setup_next_round() + self.prize_recipient.team.score += 50 + self._update_scoreboard() + return + for angle in range(0, 360, 45): + angle += random.randint(0, 45) + angle %= 360 + x = length * math.cos(math.radians(angle)) + z = length * math.sin(math.radians(angle)) + blast = Blast(position=(x, 2.2, z-2), blast_radius=3.5) + bs.timer(0.75, babase.Call(self.make_blast_ring, length-1)) + + # a method to remake the flags + def reset_flags(self) -> None: + # remake the flags + self._flag = Flag( + position=(0.0, 3.0, 1.0), touchable=True, color=(0.0, 0.0, 1.0)) + self._flag2 = Flag( + position=(0.0, 3.0, -5.0), touchable=True, color=(1.0, 0.0, 0.0)) + self._flag3 = Flag( + position=(3.0, 3.0, -2.0), touchable=True, color=(0.0, 1.0, 0.0)) + self._flag4 = Flag( + position=(-3.0, 3.0, -2.0), touchable=True, color=(1.0, 1.0, 1.0)) + self._flag5 = Flag( + position=(1.8, 3.0, 0.2), touchable=True, color=(0.0, 1.0, 1.0)) + self._flag6 = Flag( + position=(-1.8, 3.0, 0.2), touchable=True, color=(1.0, 0.0, 1.0)) + self._flag7 = Flag( + position=(1.8, 3.0, -3.8), touchable=True, color=(1.0, 1.0, 0.0)) + self._flag8 = Flag( + position=(-1.8, 3.0, -3.8), touchable=True, color=(0.0, 0.0, 0.0)) + + # a method to kill the flags + def kill_flags(self) -> None: + # destroy all the flags by erasing all references to them, + # indicated by None similar to null + self._flag.node.delete() + self._flag2.node.delete() + self._flag3.node.delete() + self._flag4.node.delete() + self._flag5.node.delete() # 132, 210 ,12 + self._flag6.node.delete() + self._flag7.node.delete() + self._flag8.node.delete() + + def _check_end_game(self) -> None: + for player in self.queue_line: + if not player.dead: + return + self.end_game() + + def spawn_player_spaz( + self, + player: PlayerT, + position: Sequence[float] = (0, 0, 0), + angle: float | None = None, + ) -> PlayerSpaz: + from babase import _math + from bascenev1._gameutils import animate + from bascenev1._coopsession import CoopSession + + angle = None + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = babase.safecolor(color, target_intensity=0.75) + + spaz = FlagBearer(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + return spaz + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # give them a nice farewell + if bs.time() < 0.5: + return + if msg.how == 'game': + return + player = msg.getplayer(Player) + bs.broadcastmessage( + diedtxt + str(player.getname()) + diedtxt2, color=player.color) + player.dead = True + if player is self.current_player: + self.round_timer = None + self.give_points_timer = None + if not msg.how is bs.DeathType.FALL: + if self._slow_motion_deaths: + bs.getactivity().globalsnode.slow_motion = True + time = 0.5 + else: + time = 0.01 + # check to see if we can end the game + self._check_end_game() + bs.timer(time, self.setup_next_round) + elif isinstance(msg, FlagPickedUpMessage): + msg.flag.last_player_to_hold = msg.node.getdelegate( + FlagBearer, True + ).getplayer(Player, True) + self._player = msg.node.getdelegate( + FlagBearer, True + ) + self.prize_recipient = msg.node.getdelegate( + FlagBearer, True + ).getplayer(Player, True) + self.kill_flags() + self.give_prize(random.randint(1, 8)) + self._round_sound.play() + self.current_player = self.prize_recipient + elif isinstance(msg, SpazBotDiedMessage): + # find out which team the last person to hold a flag was on + team = self.prize_recipient.team + # give them their points + team.score += self.bad_guy_cost + self._dingsound.play(0.5) + # update the scores + for team in self.teams: + self._scoreboard.set_team_value(team, team.score) + bs.timer(0.3, self.check_bots) + return None + + def _update_scoreboard(self) -> None: + for player in self.queue_line: + if not player.dead: + if player.team.score > 0: + self._dingsound.play() + self._scoreboard.set_team_value(player.team, player.team.score) + + def end_game(self) -> None: + if self.set: + return + self.set = True + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/plugins/minigames/frozen_one.py b/plugins/minigames/frozen_one.py new file mode 100644 index 000000000..0a98b0cb1 --- /dev/null +++ b/plugins/minigames/frozen_one.py @@ -0,0 +1,18 @@ +# Ported by your friend: Freaku + + +import babase +import bascenev1 as bs +from bascenev1lib.game.chosenone import Player, ChosenOneGame + + +# ba_meta require api 9 +# ba_meta export bascenev1.GameActivity +class FrozenOneGame(ChosenOneGame): + name = 'Frozen One' + + def _set_chosen_one_player(self, player: Player) -> None: + super()._set_chosen_one_player(player) + if hasattr(player, 'actor'): + player.actor.frozen = True + player.actor.node.frozen = 1 diff --git a/plugins/minigames/gosquad_elimination.py b/plugins/minigames/gosquad_elimination.py new file mode 100644 index 000000000..996ef6c01 --- /dev/null +++ b/plugins/minigames/gosquad_elimination.py @@ -0,0 +1,845 @@ +# Released under the MIT License. See LICENSE for details. +# initially made for gosquad server. +# +"""Elimination mini-game.""" + +# ba_meta require api 9 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import weakref +import logging +import random +import enum +from typing import TYPE_CHECKING, override + +import bascenev1 as bs + +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.bomb import Bomb + +if TYPE_CHECKING: + from typing import Any, Sequence + + +class Icon(bs.Actor): + """Creates in in-game icon on screen.""" + + def __init__( + self, + player: Player, + position: tuple[float, float], + scale: float, + *, + show_lives: bool = True, + show_death: bool = True, + name_scale: float = 1.0, + name_maxwidth: float = 115.0, + flatness: float = 1.0, + shadow: float = 1.0, + ): + super().__init__() + + self._player = weakref.ref(player) # Avoid ref loops. + self._show_lives = show_lives + self._show_death = show_death + self._name_scale = name_scale + self._outline_tex = bs.gettexture('characterIconMask') + + icon = player.get_icon() + self.node = bs.newnode( + 'image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter', + }, + ) + self._name_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': bs.Lstr(value=player.getname()), + 'color': bs.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': flatness, + 'h_attach': 'center', + 'v_attach': 'bottom', + }, + ) + if self._show_lives: + self._lives_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': 'x0', + 'color': (1, 1, 0.5), + 'h_align': 'left', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom', + }, + ) + self.set_position_and_scale(position, scale) + + def set_position_and_scale( + self, position: tuple[float, float], scale: float + ) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + if self._show_lives: + self._lives_text.position = ( + position[0] + scale * 10.0, + position[1] - scale * 43.0, + ) + self._lives_text.scale = 1.0 * scale + + def update_for_lives(self) -> None: + """Update for the target player's current lives.""" + player = self._player() + if player: + lives = player.lives + else: + lives = 0 + if self._show_lives: + if lives > 0: + self._lives_text.text = 'x' + str(lives - 1) + else: + self._lives_text.text = '' + if lives == 0: + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + def handle_player_spawned(self) -> None: + """Our player spawned; hooray!""" + if not self.node: + return + self.node.opacity = 1.0 + self.update_for_lives() + + def handle_player_died(self) -> None: + """Well poo; our player died.""" + if not self.node: + return + if self._show_death: + bs.animate( + self.node, + 'opacity', + { + 0.00: 1.0, + 0.05: 0.0, + 0.10: 1.0, + 0.15: 0.0, + 0.20: 1.0, + 0.25: 0.0, + 0.30: 1.0, + 0.35: 0.0, + 0.40: 1.0, + 0.45: 0.0, + 0.50: 1.0, + 0.55: 0.2, + }, + ) + player = self._player() + lives = player.lives if player else 0 + if lives == 0: + bs.timer(0.6, self.update_for_lives) + + @override + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + self.node.delete() + return None + return super().handlemessage(msg) + + +class BombType(enum.Enum): + DEFAULT = 0 + NORMAL = 1 + STICKY = 2 + TRIGGER = 3 + ICE = 4 + + @property + def as_str(self) -> str: + return { + BombType.DEFAULT: "default", + BombType.NORMAL: "normal", + BombType.STICKY: "sticky", + BombType.TRIGGER: "impact", + BombType.ICE: "ice", + }[self] + + @staticmethod + def from_int(value: int) -> "BombType": + return BombType(value) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.lives = 0 + self.icons: list[Icon] = [] + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: int | None = None + self.spawn_order: list[Player] = [] + + +# ba_meta export bascenev1.GameActivity +class EliminationGame(bs.TeamGameActivity[Player, Team]): + """Game type where last player(s) left alive win.""" + + name = 'Gosquad Elimination' + description = 'Elimination game big bombs meteor shower.' + scoreconfig = bs.ScoreConfig( + label='Survived', scoretype=bs.ScoreType.SECONDS, none_is_winner=True + ) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + allow_mid_activity_joins = False + + @override + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[bs.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=3, + min_value=1, + max_value=10, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=True), + bs.BoolSetting('Equip Gloves', default=True), + bs.BoolSetting('Equip Speed', default=False), + bs.BoolSetting('Equip Shield', default=False), + bs.BoolSetting('Meteor Shower', default=True), + bs.IntChoiceSetting( + 'Meteor Delay', + choices=[ + ('None', 0), + ('15 Seconds', 15), + ('30 Seconds', 30), + ('45 Seconds', 45), + ('1 Minutes', 60), + ('1.5 Minutes', 90), + ('2 Minutes', 120), + + ], + default=0, + ), + bs.IntChoiceSetting( + 'Bomb Count', + choices=[ + ('Default', 0), + ('1', 1), + ('2', 2), + ('3', 3), + ('4', 4), + ('5', 5), + ('6', 6), + ], + default=0, + ), + bs.IntChoiceSetting( + 'Bomb Type', + choices=[ + ('Default', 0), + ('Normal', 1), + ('Sticky', 2), + ('Trigger', 3), + ('Ice', 4), + ], + default=0, + ), + bs.BoolSetting('Revive Eliminated Players', default=True), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Lives', default=False) + ) + return settings + + @override + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession + ) + + @override + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + # (Pylint Bug?) pylint: disable=missing-function-docstring + assert bs.app.classic is not None + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._start_time: float | None = None + self._vs_text: bs.Actor | None = None + self._round_end_timer: bs.Timer | None = None + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = int(settings['Lives Per Player']) + self._time_limit = float(settings['Time Limit']) + self._balance_total_lives = bool( + settings.get('Balance Total Lives', False) + ) + self._solo_mode = bool(settings.get('Solo Mode', False)) + self._equip_gloves = bool(settings.get('Equip Gloves', False)) + self._equip_speed = bool(settings.get('Equip Speed', False)) + self._equip_shield = bool(settings.get('Equip Shield', False)) + self._meteor_shower = bool(settings.get('Meteor Shower', False)) + self._meteor_start_time = float(settings['Meteor Delay']) + self._bomb_count = int(settings['Bomb Count']) + bomb_type_raw = BombType.from_int(settings.get('Bomb Type', 0)) + self._bomb_type = str(bomb_type_raw.as_str) + self._revive_eliminated = bool(settings.get('Revive Eliminated Players', True)) + self._revive_eliminated_timer: bs.Timer | None = None + + self._bomb_time = 3.0 + self._bomb_scale = 0.1 + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL + ) + + @override + def get_instance_description(self) -> str | Sequence: + # (Pylint Bug?) pylint: disable=missing-function-docstring + return ( + 'Last team standing wins.' + if isinstance(self.session, bs.DualTeamSession) + else 'Last one standing wins.' + ) + + @override + def get_instance_description_short(self) -> str | Sequence: + # (Pylint Bug?) pylint: disable=missing-function-docstring + return ( + 'last team standing wins' + if isinstance(self.session, bs.DualTeamSession) + else 'last one standing wins' + ) + + @override + def on_player_join(self, player: Player) -> None: + # (Pylint Bug?) pylint: disable=missing-function-docstring + player.lives = self._lives_per_player + + if self._solo_mode: + player.team.spawn_order.append(player) + self._update_solo_mode() + else: + # Create our icon and spawn. + player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: + self.spawn_player(player) + + # Don't waste time doing this until begin. + if self.has_begun(): + self._update_icons() + + @override + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + if self._solo_mode: + self._vs_text = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': bs.Lstr(resource='vsText'), + }, + ) + ) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if ( + isinstance(self.session, bs.DualTeamSession) + and self._balance_total_lives + and self.teams[0].players + and self.teams[1].players + ): + if self._get_total_team_lives( + self.teams[0] + ) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while self._get_total_team_lives( + lesser_team + ) < self._get_total_team_lives(greater_team): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._update_icons() + if self._meteor_shower: + bs.timer(self._meteor_start_time, self._initiate_bomb) + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + bs.timer(1.0, self._update, repeat=True) + + def _update_solo_mode(self) -> None: + # For both teams, find the first player on the spawn order list with + # lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + break + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p + for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon( + player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False, + ) + ) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def _get_spawn_point(self, player: Player) -> bs.Vec3 | None: + del player # Unused. + + # In solo-mode, if there's an existing live player on the map, spawn at + # whichever spot is farthest from them (keeps the action spread out). + if self._solo_mode: + living_player = None + living_player_pos = None + for team in self.teams: + for tplayer in team.players: + if tplayer.is_alive(): + assert tplayer.node + ppos = tplayer.node.position + living_player = tplayer + living_player_pos = ppos + break + if living_player: + assert living_player_pos is not None + player_pos = bs.Vec3(living_player_pos) + points: list[tuple[float, bs.Vec3]] = [] + for team in self.teams: + start_pos = bs.Vec3(self.map.get_start_position(team.id)) + points.append( + ((start_pos - player_pos).length(), start_pos) + ) + # Hmm.. we need to sort vectors too? + points.sort(key=lambda x: x[0]) + return points[-1][1] + return None + + def _initiate_bomb(self) -> None: + delay = 1.0 + bs.timer(delay, self._decrement_bomb_time, repeat=True) + bs.timer(delay, self._increment_bomb_scale, repeat=True) + + # Kick off the first wave in a few seconds. + self._set_bomb_timer() + + def _set_bomb_timer(self) -> None: + bs.timer(self._bomb_time, self._drop_bomb_cluster) + + def _drop_bomb_cluster(self) -> None: + # Random note: code like this is a handy way to plot out extents + # and debug things. + loc_test = False + if loc_test: + bs.newnode('locator', attrs={'position': (0.0, 20.0, -20.0)}) + + # Drop a single bomb. + # Drop them somewhere within our bounds with velocity pointing + # toward the opposite side. + # I took this specific calculation from a work of byAngel. + # Credit goes to him. + if self.map.getname() == 'Hockey Stadium': + pos = (random.randrange(-11, 12), 6, random.randrange(-4, 5)) + elif self.map.getname() == 'Football Stadium': + pos = (random.randrange(-10, 11), 6, random.randrange(-5, 6)) + elif self.map.getname() == 'The Pad': + pos = (random.randrange(-3, 4), 10, random.randrange(-8, 4)) + elif self.map.getname() == 'Doom Shroom': + pos = (random.randrange(-7, 8), 8, random.randrange(-7, 1)) + elif self.map.getname() == 'Lake Frigid': + pos = (random.randrange(-7, 8), 8, random.randrange(-7, 3)) + elif self.map.getname() == 'Tower D': + pos = (random.randrange(-7, 8), 8, random.randrange(-5, 5)) + elif self.map.getname() == 'Step Right Up': + pos = (random.randrange(-7, 8), 10, random.randrange(-8, 3)) + elif self.map.getname() == 'Courtyard': + pos = (random.randrange(-7, 8), 10, random.randrange(-6, 4)) + elif self.map.getname() == 'Rampage': + pos = (random.randrange(-7, 8), 11, random.randrange(-5, -2)) + else: + pos = (random.randrange(-7, 8), 6, random.randrange(-5, 6)) + + dropdir = (-1.0 if pos[0] > 0 else 1.0) + vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0) + + self._drop_bomb(pos, vel) + self._set_bomb_timer() + + def _drop_bomb( + self, position: Sequence[float], velocity: Sequence[float] + ) -> None: + bomb_type = random.choice([ + 'land_mine', 'land_mine', 'tnt', 'tnt', + 'impact', 'sticky', 'normal', + ]) + bomb = Bomb( + position=position, + velocity=velocity, + bomb_type=bomb_type, + blast_radius=3.0 if bomb_type == 'impact' else self._bomb_scale, + ).autoretain() + + if bomb_type != 'impact': + bs.animate(bomb.node, 'mesh_scale', { + 0.0: 0.2, + 0.7: 0.2, + 1.0: self._bomb_scale} + ) + + if bomb_type == 'land_mine': + bs.timer(1.2, bomb.arm) + + def _decrement_bomb_time(self) -> None: + self._bomb_time = max(2.0, self._bomb_time * 0.8) + + def _increment_bomb_scale(self) -> None: + self._bomb_scale = min(8.0, self._bomb_scale * 1.2) + + @override + def spawn_player(self, player: Player) -> bs.Actor: + """Spawn a player (override).""" + actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) + if not self._solo_mode: + bs.timer(0.3, bs.CallStrict(self._print_lives, player)) + + if self._equip_gloves: + actor.equip_boxing_gloves() + if self._equip_shield: + actor.equip_shields() + + actor.node.hockey = self._equip_speed + actor.bomb_count = actor.bomb_count if self._bomb_count == 0 else self._bomb_count + bomb_type = actor.bomb_type if self._bomb_type == 'default' else self._bomb_type + actor.bomb_type = actor.bomb_type_default = bomb_type + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + return actor + + def _print_lives(self, player: Player) -> None: + from bascenev1lib.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + popuptext.PopupText( + 'x' + str(player.lives - 1), + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=player.node.position, + ).autoretain() + + @override + def on_player_leave(self, player: Player) -> None: + # (Pylint Bug?) pylint: disable=missing-function-docstring + super().on_player_leave(player) + player.icons = [] + + # Remove us from spawn-order. + if self._solo_mode: + if player in player.team.spawn_order: + player.team.spawn_order.remove(player) + + # Update icons in a moment since our team will be gone from the + # list then. + bs.timer(0, self._update_icons) + + # If the player to leave was the last in spawn order and had + # their final turn currently in-progress, mark the survival time + # for their team. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - self._start_time) + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + @override + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + logging.exception( + "Got lives < 0 in Elim; this shouldn't happen. solo: %s", + self._solo_mode, + ) + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if self._solo_mode or player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + # If the whole team is now dead, mark their survival time. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int( + bs.time() - self._start_time + ) + else: + # Otherwise, in regular mode, respawn. + if not self._solo_mode: + self.respawn_player(player) + + # In solo, put ourself at the back of the spawn order. + if self._solo_mode: + try: + player.team.spawn_order.remove(player) + player.team.spawn_order.append(player) + except: + pass + + def _update(self) -> None: + if self._solo_mode: + # For both teams, find the first player on the spawn order + # list with lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + self._update_icons() + break + + # If we're down to 1 or fewer living teams, start a timer to end + # the game (allows the dust to settle and draws to occur if deaths + # are close enough). + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0.5, self.end_game) + + # Start the 120s timer when only 2 players remain. + if ( + len(self._get_living_players()) == 2 and + self._revive_eliminated and + len(self.players) >= 4 + ): + self._revive_eliminated = False + bs.broadcastmessage( + "Be ready! ⚔️ 2 random eliminated players may rejoin the game in 2 minutes.", + color=(1, 0.7, 0.1) + ) + self._revive_eliminated_timer = bs.BaseTimer( + 120, bs.WeakCallStrict(self._revive_random_players)) + + def _get_living_teams(self) -> list[Team]: + return [ + team + for team in self.teams + if len(team.players) > 0 + and any(player.lives > 0 for player in team.players) + ] + + def _get_living_players(self) -> list[Player]: + return [ + player for player in self.players if player.lives > 0 + ] + + def _revive_random_players(self) -> None: + # Cancel if game already ended + if self.has_ended(): + return + + eliminated_players = [p for p in self.players if p.exists() and p.lives <= 0] + if not eliminated_players: + bs.broadcastmessage("No eliminated players available.", color=(0.7, 0.7, 0.7)) + return + + # Pick up to 2 random eliminated players + revived = random.sample(eliminated_players, min(2, len(eliminated_players))) + for player in revived: + player.lives = 1 + if not self._solo_mode: + self.spawn_player(player) + for icon in player.icons: + icon.update_for_lives() + assert icon.node, icon._name_text + icon._name_text.opacity = 1.0 + icon.node.color = (1, 1, 1) + icon.node.opacity = 1.0 + bs.broadcastmessage(f"{player.getname(full=True)} rejoined the game!", color=(0, 1, 0)) + + self._update_icons() + self._revive_eliminated = True # Allow the event to trigger again later + + @override + def end_game(self) -> None: + """End the game.""" + if self.has_ended(): + return + results = bs.GameResults() + self._vs_text = None # Kill our 'vs' if its there. + for team in self.teams: + results.set_team_score(team, team.survival_seconds) + self.end(results=results) diff --git a/plugins/minigames/gravity_falls.py b/plugins/minigames/gravity_falls.py new file mode 100644 index 000000000..e4c7e44a9 --- /dev/null +++ b/plugins/minigames/gravity_falls.py @@ -0,0 +1,35 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# Made by MattZ45986 on GitHub +# Ported by: Freaku / @[Just] Freak#4999 + + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.game.elimination import EliminationGame + + +# ba_meta require api 8 +# ba_meta export bascenev1.GameActivity +class GFGame(EliminationGame): + name = 'Gravity Falls' + + def spawn_player(self, player): + actor = self.spawn_player_spaz(player, (0, 5, 0)) + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + bs.timer(1, babase.Call(self.raise_player, player)) + return actor + + def raise_player(self, player): + if player.is_alive(): + try: + player.actor.node.handlemessage( + "impulse", player.actor.node.position[0], player.actor.node.position[1]+.5, player.actor.node.position[2], 0, 5, 0, 3, 10, 0, 0, 0, 5, 0) + except: + pass + bs.timer(0.05, babase.Call(self.raise_player, player)) diff --git a/plugins/minigames/handball.py b/plugins/minigames/handball.py new file mode 100644 index 000000000..dfb439d83 --- /dev/null +++ b/plugins/minigames/handball.py @@ -0,0 +1,383 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Hockey game and support classes.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, Union + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + + +class Puck(bs.Actor): + """A lovely giant hockey puck.""" + + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch: dict[int, Player] = {} + self.scored = False + assert activity is not None + assert isinstance(activity, HockeyGame) + pmats = [shared.object_material, activity.puck_material] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': activity.puck_mesh, + 'color_texture': activity.puck_tex, + 'body': 'sphere', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.8, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class HockeyGame(bs.TeamGameActivity[Player, Team]): + """Ice hockey game.""" + + name = 'Handball' + description = 'Score some goals.' + available_settings = [ + bs.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps('hockey') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = bs.getsound('cheer') + self._chant_sound = bs.getsound('crowdChant') + self._foghorn_sound = bs.getsound('foghorn') + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self.puck_mesh = bs.getmesh('bomb') + self.puck_tex = bs.gettexture('bonesColor') + self._puck_sound = bs.getsound('metalHit') + self._epic_mode = bool(settings['Epic Mode']) + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.FOOTBALL) + self.puck_material = bs.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', False)) + self.puck_material = bs.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.puck_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._puck_sound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', bs.DieMessage()))) + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[list[bs.NodeActor]] = None + self._puck: Optional[Puck] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Score a goal.' + return 'Score ${ARG1} goals.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'score a goal' + return 'score ${ARG1} goals', self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._puck_spawn_pos = self.map.get_flag_position(None) + self._spawn_puck() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': defs.boxes['goal1'][0:3], + 'scale': defs.boxes['goal1'][6:9], + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': defs.boxes['goal2'][0:3], + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._update_scoreboard() + self._chant_sound.play() + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = bs.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + puck.last_players_to_touch[player.team.id] = player + + def _kill_puck(self) -> None: + self._puck = None + + def _handle_score(self) -> None: + """A point has been scored.""" + + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = bs.getcollision().sourcenode + index = 0 + for index, score_region in enumerate(self._score_regions): + if region == score_region.node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 100, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + self._foghorn_sound.play() + self._cheer_sound.play() + + self._puck.scored = True + + # Kill the puck (it'll respawn itself shortly). + bs.timer(1.0, self._kill_puck) + + light = bs.newnode('light', + attrs={ + 'position': bs.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + bs.timer(1.0, light.delete) + + bs.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + bs.timer(3.0, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + light = bs.newnode('light', + attrs={ + 'position': self._puck_spawn_pos, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) + + def _spawn_puck(self) -> None: + self._swipsound.play() + self._whistle_sound.play() + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) diff --git a/plugins/minigames/hot_bomb.py b/plugins/minigames/hot_bomb.py new file mode 100644 index 000000000..ba68c09b2 --- /dev/null +++ b/plugins/minigames/hot_bomb.py @@ -0,0 +1,1668 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Hot Bomb game by SEBASTIAN2059 and zPanxo""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import random + +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.spaz import PickupMessage, BombDiedMessage + +from bascenev1._messages import StandMessage + +import bascenev1 as bs +import _bascenev1 as _bs +import _babase + + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class BallDiedMessage: + """Inform something that a ball has died.""" + + def __init__(self, ball: Ball): + self.ball = ball + + +class ExplodeHitMessage: + """Tell an object it was hit by an explosion.""" + + +class Ball(bs.Actor): + """A lovely bomb mortal""" + + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0), timer: int = 5, d_time=0.2, color=(1, 1, 1)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + self.explosion_material = bs.Material() + self.explosion_material.add_actions( + conditions=( + 'they_have_material', shared.object_material + ), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('message', 'our_node', 'at_connect', ExplodeHitMessage()), + ), + ) + + bs.getsound('scamper01').play(volume=0.4) + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + assert activity is not None + assert isinstance(activity, HotBombGame) + pmats = [shared.object_material, activity.ball_material] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': activity.ball_mesh, + 'color_texture': activity.ball_tex, + 'body': activity.ball_body, + 'body_scale': 1.0 if activity.ball_body == 'sphere' else 0.8, + 'density': 1.0 if activity.ball_body == 'sphere' else 1.2, + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.5, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + } + ) + self._animate = None + self.scale = 1.0 if activity.ball_body == 'sphere' else 0.8 + + self.color_l = (1, 1, 1) + self.light = bs.newnode('light', + owner=self.node, + attrs={ + 'color': color, + 'volume_intensity_scale': 0.4, + 'intensity': 0.5, + 'radius': 0.10 + } + ) + self.node.connectattr('position', self.light, 'position') + self.animate_light = None + + self._particles = bs.Timer(0.1, call=bs.WeakCall(self.particles), repeat=True) + self._sound_effect = bs.Timer(4, call=bs.WeakCall(self.sound_effect), repeat=True) + + self.d_time = d_time + + if timer is not None: + timer = int(timer) + self._timer = timer + self._counter: Optional[bs.Node] + if self._timer is not None: + self._count = self._timer + self._tick_timer = bs.Timer(1.0, + call=bs.WeakCall(self._tick), + repeat=True) + m = bs.newnode('math', owner=self.node, attrs={ + 'input1': (0, 0.6, 0), 'operation': 'add'}) + self.node.connectattr('position', m, 'input2') + self._counter = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': str(timer), + 'in_world': True, + 'shadow': 1.0, + 'flatness': 0.7, + 'color': (1, 1, 1), + 'scale': 0.013, + 'h_align': 'center' + } + ) + m.connectattr('output', self._counter, 'position') + else: + self._counter = None + + def particles(self): + if self.node: + bs.emitfx( + position=self.node.position, + velocity=(0, 3, 0), + count=9, + scale=2.5, + spread=0.2, + chunk_type='sweat' + ) + + def sound_effect(self): + if self.node: + bs.getsound('scamper01').play(volume=0.4) + + def explode(self, color=(3, 1, 0)) -> None: + sound = random.choice(['explosion01', 'explosion02', + 'explosion03', 'explosion04', 'explosion05']) + bs.getsound(sound).play(volume=1) + bs.emitfx(position=self.node.position, + velocity=(0, 10, 0), + count=100, + scale=1.0, + spread=1.0, + chunk_type='spark') + explosion = bs.newnode( + 'explosion', + attrs={ + 'position': self.node.position, + 'velocity': (0, 0, 0), + 'radius': 2.0, + 'big': False, + 'color': color + } + ) + bs.timer(1.0, explosion.delete) + if color == (5, 1, 0): + color = (1, 0, 0) + self.activity._handle_score(1) + else: + color = (0, 0, 1) + self.activity._handle_score(0) + + scorch = bs.newnode( + 'scorch', + attrs={ + 'position': self.node.position, + 'size': 1.0, + 'big': True, + 'color': color, + 'presence': 1 + } + ) + + # Set our position a bit lower so we throw more things upward. + rmats = (self.explosion_material,) + self.region = bs.newnode( + 'region', + delegate=self, + attrs={ + 'position': (self.node.position[0], self.node.position[1] - 0.1, self.node.position[2]), + 'scale': (2.0, 2.0, 2.0), + 'type': 'sphere', + 'materials': rmats + }, + ) + bs.timer(0.05, self.region.delete) + + def _tick(self) -> None: + c = self.color_l + c2 = (2.5, 1.5, 0) + if c[2] != 0: + c2 = (0, 2, 3) + if self.node: + if self._count == 1: + pos = self.node.position + color = (5, 1, 0) if pos[0] < 0 else (0, 1, 5) + self.explode(color=color) + return + if self._count > 0: + self._count -= 1 + assert self._counter + self._counter.text = str(self._count) + bs.getsound('tick').play() + if self._count == 1: + self._animate = bs.animate( + self.node, + 'mesh_scale', + { + 0: self.node.mesh_scale, + 0.1: 1.5, + 0.2: self.scale + }, + loop=True + ) + self.animate_light = bs.animate_array( + self.light, + 'color', + 3, + { + 0: c, + 0.1: c2, + 0.2: c + }, + loop=True + ) + else: + self._animate = bs.animate( + self.node, + 'mesh_scale', + { + 0: self.node.mesh_scale, + 0.5: 1.5, + 1.0: self.scale + }, + loop=True + ) + self.animate_light = bs.animate_array( + self.light, + 'color', + 3, + { + 0: c, + 0.2: c2, + 0.5: c, + 1.0: c + }, + loop=True + ) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if not self.node: + return + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(BallDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, bs.PickedUpMessage): + d = self.d_time + + def damage(): + if (msg is not None and msg.node.exists() + and msg.node.getdelegate(PlayerSpaz).hitpoints > 0): + spaz = msg.node.getdelegate(PlayerSpaz) + spaz.node.color = (spaz.node.color[0]-0.1, + spaz.node.color[1]-0.1, spaz.node.color[2]-0.1) + if spaz.node.hold_node != self.node: + self.handlemessage(bs.DroppedMessage(spaz.node)) + if spaz.hitpoints > 10000: + bs.getsound('fuse01').play(volume=0.3) + spaz.hitpoints -= 10000 + spaz._last_hit_time = None + spaz._num_time_shit = 0 + spaz.node.hurt = 1.0 - float(spaz.hitpoints) / spaz.hitpoints_max + else: + spaz.handlemessage(bs.DieMessage()) + bs.emitfx( + position=msg.node.position, + velocity=(0, 3, 0), + count=20 if d == 0.2 else 25 if d == 0.1 else 30 if d == 0.05 else 15, + scale=1.0, + spread=0.2, + chunk_type='sweat') + else: + self.damage_timer = None + + self.damage_timer = bs.Timer(self.d_time, damage, repeat=True) + + elif isinstance(msg, bs.DroppedMessage): + spaz = msg.node.getdelegate(PlayerSpaz) + self.damage_timer = None + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + + elif isinstance(msg, ExplodeHitMessage): + node = bs.getcollision().opposingnode + if not self.node: + return + nodepos = self.region.position + mag = 2000.0 + + node.handlemessage( + bs.HitMessage( + pos=nodepos, + velocity=(0, 0, 0), + magnitude=mag, + hit_type='explosion', + hit_subtype='normal', + radius=2.0 + ) + ) + self.handlemessage(bs.DieMessage()) + else: + super().handlemessage(msg) + +### HUMAN### + + +class NewPlayerSpaz(PlayerSpaz): + + move_mult = 1.0 + reload = True + extra_jump = True + # calls + + def impulse(self): + self.reload = False + p = self.node + self.node.handlemessage( + "impulse", + p.position[0], p.position[1]+40, p.position[2], + 0, 0, 0, + 160, 0, 0, 0, + 0, 205, 0) + bs.timer(0.4, self.refresh) + + def refresh(self): + self.reload = True + + def drop_bomb(self) -> Optional[Bomb]: + + if (self.land_mine_count <= 0 and self.bomb_count <= 0) or self.frozen: + return None + assert self.node + pos = self.node.position_forward + vel = self.node.velocity + + if self.land_mine_count > 0: + dropping_bomb = False + self.set_land_mine_count(self.land_mine_count - 1) + bomb_type = 'land_mine' + else: + dropping_bomb = True + bomb_type = self.bomb_type + + if bomb_type == 'banana': + bs.getsound('penguinHit1').play(volume=0.3) + bomb = NewBomb(position=(pos[0], pos[1] + 0.7, pos[2]), + velocity=(vel[0], vel[1], vel[2]), + bomb_type=bomb_type, + radius=1.0, + source_player=self.source_player, + owner=self.node) + else: + bomb = Bomb(position=(pos[0], pos[1] - 0.0, pos[2]), + velocity=(vel[0], vel[1], vel[2]), + bomb_type=bomb_type, + blast_radius=self.blast_radius, + source_player=self.source_player, + owner=self.node).autoretain() + + assert bomb.node + if dropping_bomb: + self.bomb_count -= 1 + bomb.node.add_death_action( + bs.WeakCall(self.handlemessage, BombDiedMessage())) + self._pick_up(bomb.node) + + try: + for clb in self._dropped_bomb_callbacks: + clb(self, bomb) + except Exception: + return + + return bomb + + def on_jump_press(self) -> None: + if not self.node: + return + self.node.jump_pressed = True + self._turbo_filter_add_press('jump') + + if self.reload and self.extra_jump: + self.impulse() + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, PickupMessage): + if not self.node: + return None + try: + collision = bs.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + except bs.NotFoundError: + return True + if opposingnode.getnodetype() == 'spaz': + player = opposingnode.getdelegate(PlayerSpaz, True).getplayer(Player, True) + if player.actor.shield: + return None + super().handlemessage(msg) + return super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +lang = bs.app.lang.language +if lang == 'Spanish': + name = 'Hot Bomb' + description = 'Consigue explotar la bomba en\nel equipo enemigo para ganar.' + join_description = 'Deshazte de la bomba cuanto antes.' + join_description_l = 'Deshazte de la bomba cuanto antes.' + view_description = 'Estalla la bomba en el equipo rival' + view_description_l = 'Estalla ${ARG1} veces la bomba en el equipo rival' + bomb_timer = 'Temporizador' + space_wall = 'Espacio Debajo de la Red' + num_bones = 'Huesos Distractores' + b_count = ['Nada', 'Pocos', 'Muchos'] + shield = 'Inmortalidad' + bomb = 'Habilitar Bananas' + boxing_gloves = 'Equipar Guantes de Boxeo' + difficulty = 'Dificultad' + difficulty_o = ['Fácil', 'Difícil', 'Chernobyl'] + wall_color = 'Color de la Red' + w_c = ['Verde', 'Rojo', 'Naranja', 'Amarillo', 'Celeste', 'Azul', 'Rosa', 'Gris'] + ball_body = 'Tipo de Hot Bomb' + body = ['Esfera', 'Cubo'] + +else: + name = 'Hot Bomb' + description = 'Get the bomb to explode on\nthe enemy team to win.' + join_description = 'Get rid of the bomb as soon as possible.' + join_description_l = 'Get rid of the bomb as soon as possible.' + view_description = 'Explode the bomb in the enemy team' + view_description_l = 'Explode the bomb ${ARG1} times in the enemy team' + bomb_timer = 'Timer' + space_wall = 'Space Under the Mesh' + num_bones = 'Distractor Bones' + b_count = ['None', 'Few', 'Many'] + shield = 'Immortality' + bomb = 'Enable Bananas' + difficulty = 'Difficulty' + difficulty_o = ['Easy', 'Hard', 'Chernobyl'] + wall_color = 'Mesh Color' + w_c = ['Green', 'Red', 'Orange', 'Yellow', 'Light blue', 'Blue', 'Ping', 'Gray'] + ball_body = 'Type of Hot Bomb' + body = ['Sphere', 'Box'] + + +# ba_meta export bascenev1.GameActivity +class HotBombGame(bs.TeamGameActivity[Player, Team]): + """New game.""" + + name = name + description = description + available_settings = [ + bs.IntSetting( + 'Score to Win', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 3.0), + ], + default=0.5, + + ), + bs.FloatChoiceSetting( + difficulty, + choices=[ + (difficulty_o[0], 0.15), + (difficulty_o[1], 0.04), + (difficulty_o[2], 0.01), + ], + default=0.15, + + ), + bs.IntChoiceSetting( + bomb_timer, + choices=[(str(choice)+'s', choice) for choice in range(2, 11)], + default=5, + + ), + bs.IntChoiceSetting( + num_bones, + choices=[ + (b_count[0], 0), + (b_count[1], 2), + (b_count[2], 5), + ], + default=2, + + ), + bs.IntChoiceSetting( + ball_body, + choices=[(b, body.index(b)) for b in body], + default=0, + ), + bs.IntChoiceSetting( + wall_color, + choices=[(color, w_c.index(color)) for color in w_c], + default=0, + + ), + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting(space_wall, default=True), + bs.BoolSetting(bomb, default=True), + bs.BoolSetting(shield, default=False), + + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._bomb_timer = int(settings[bomb_timer]) + self._space_under_wall = bool(settings[space_wall]) + self._num_bones = int(settings[num_bones]) + self._shield = bool(settings[shield]) + self._bomb = bool(settings[bomb]) + self.damage_time = float(settings[difficulty]) + self._epic_mode = bool(settings['Epic Mode']) + self._wall_color = int(settings[wall_color]) + self._ball_body = int(settings[ball_body]) + + self.bodys = ['sphere', 'crate'] + self.meshs = ['bombSticky', 'powerupSimple'] + + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = bs.getsound('cheer') + self._chant_sound = bs.getsound('crowdChant') + self._foghorn_sound = bs.getsound('foghorn') + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self.ball_mesh = bs.getmesh(self.meshs[self._ball_body]) + self.ball_body = self.bodys[self._ball_body] + self.ball_tex = bs.gettexture('powerupCurse') + self._ball_sound = bs.getsound('splatter') + + self.last_point = None + self.colors = [(0.25, 0.5, 0.25), (1, 0.15, 0.15), (1, 0.5, 0), (1, 1, 0), + (0.2, 1, 1), (0.1, 0.1, 1), (1, 0.3, 0.5), (0.5, 0.5, 0.5)] + # + self.slow_motion = self._epic_mode + + self.ball_material = bs.Material() + self.ball_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.ball_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.ball_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.ball_material.add_actions( + conditions=( + 'they_have_material', shared.footing_material + ), + actions=( + 'impact_sound', self._ball_sound, 0.2, 4 + ) + ) + + # Keep track of which player last touched the ball + self.ball_material.add_actions( + conditions=( + 'they_have_material', shared.player_material + ), + actions=( + ('call', 'at_connect', self._handle_ball_player_collide), + ) + ) + + # We want the ball to kill powerups; not get stopped by them + self.ball_material.add_actions( + conditions=( + 'they_have_material', PowerupBoxFactory.get().powerup_material), + actions=( + ('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', bs.DieMessage()) + ) + ) + + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=( + 'they_have_material', self.ball_material + ), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score) + ) + ) + ##### + self._check_region_material = bs.Material() + self._check_region_material.add_actions( + conditions=( + 'they_have_material', self.ball_material + ), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._reset_count) + ) + ) + + self._reaction_material = bs.Material() + self._reaction_material.add_actions( + conditions=( + 'they_have_material', shared.player_material + ), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._reaction) + ) + ) + + self._reaction_material.add_actions( + conditions=( + 'they_have_material', HealthFactory.get().health_material + ), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + ) + ) + + self._collide = bs.Material() + self._collide.add_actions( + conditions=( + ('they_are_different_node_than_us', ), + 'and', + ('they_have_material', shared.player_material), + ), + actions=( + ('modify_part_collision', 'collide', True) + ) + ) + + self._wall_material = bs.Material() + self._wall_material.add_actions( + conditions=( + 'we_are_older_than', 1 + ), + actions=( + ('modify_part_collision', 'collide', True) + ) + ) + + self.ice_material = bs.Material() + self.ice_material.add_actions( + actions=( + 'modify_part_collision', 'friction', 0.05 + ) + ) + + self._ball_spawn_pos: Optional[Sequence[float]] = None + self._ball: Optional[Ball] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return join_description + return join_description_l, self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return view_description + return view_description_l, self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self._ball_spawn_pos = (random.choice([-5, 5]), 4, 0) + bs.timer(5, self._spawn_ball) + bs.timer(0.1, self.update_ball, repeat=True) + self.add_game_complements() + self.add_map_complements() + self._update_scoreboard() + self._chant_sound.play() + + def _reaction(self): + node: bs.Node = bs.getcollision().opposingnode + bs.getsound('hiss').play(volume=0.75) + + node.handlemessage( + "impulse", + node.position[0], node.position[1], node.position[2], + -node.velocity[0]*2, -node.velocity[1], -node.velocity[2], + 100, 100, 0, 0, + -node.velocity[0], -node.velocity[1], -node.velocity[2] + ) + + bs.emitfx( + position=node.position, + count=20, + scale=1.5, + spread=0.5, + chunk_type='sweat' + ) + + def add_game_complements(self): + HealthBox( + position=(-1, 3.5, -5+random.random()*10) + ) + HealthBox( + position=(1, 3.5, -5+random.random()*10) + ) + ### + g = 0 + while g < self._num_bones: + b = 0 + Torso( + position=(-6+random.random()*12, 3.5, -5+random.random()*10) + ) + while b < 6: + Bone( + position=(-6+random.random()*12, 2, -5+random.random()*10), + style=b + ) + b += 1 + g += 1 + ######################## + self.wall_color = self.colors[self._wall_color] + part_of_wall = bs.newnode( + 'locator', + attrs={ + 'shape': 'box', + 'position': (-7.169, 0.5, 0.5), + 'color': self.wall_color, + 'opacity': 1, + 'drawShadow': False, + 'draw_beauty': True, + 'additive': False, + 'size': [14.7, 2, 16] + } + ) + part_of_wall2 = bs.newnode( + 'locator', + attrs={ + 'shape': 'box', + 'position': (0, -13.51, 0.5) if self._space_under_wall else (0, -35.540, 0.5), + 'color': self.wall_color, + 'opacity': 1, + 'drawShadow': False, + 'draw_beauty': True, + 'additive': False, + 'size': [0.3, 30, 13] if self._space_under_wall else [0.3, 75, 13] + } + ) + wall = bs.newnode( + 'region', + attrs={ + 'position': (0, 1.11, 0.5) if self._space_under_wall else (0, 0.75, 0.5), + 'scale': (0.3, 0.75, 13) if self._space_under_wall else (0.3, 1.5, 13), + 'type': 'box', + 'materials': (self._wall_material, self._reaction_material) + } + ) + # RESET REGION + pos = (0, 5.3, 0) + bs.newnode( + 'region', + attrs={ + 'position': pos, + 'scale': (0.001, 15, 12), + 'type': 'box', + 'materials': [self._check_region_material, self._reaction_material] + } + ) + + bs.newnode( + 'region', + attrs={ + 'position': pos, + 'scale': (0.3, 15, 12), + 'type': 'box', + 'materials': [self._collide] + } + ) + + def add_map_complements(self): + # TEXT + text = bs.newnode('text', + attrs={'position': (0, 2.5, -6), + 'text': 'Hot Bomb by\nSEBASTIAN2059 and zPanxo', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 0.7, + 'color': (1.91, 1.31, 0.59), + 'opacity': 0.25-0.15, + 'scale': 0.013+0.007, + 'h_align': 'center'}) + walls_data = { + 'w1': [ + (11, 5.5, 0), + (4.5, 11, 13) + ], + 'w2': [ + (-11, 5.5, 0), + (4.5, 11, 13) + ], + 'w3': [ + (0, 5.5, -6.1), + (19, 11, 1) + ], + 'w4': [ + (0, 5.5, 6.5), + (19, 11, 1) + ], + } + for i in walls_data: + w = bs.newnode( + 'region', + attrs={ + 'position': walls_data[i][0], + 'scale': walls_data[i][1], + 'type': 'box', + 'materials': (self._wall_material,) + } + ) + + for i in [-5, -2.5, 0, 2.5, 5]: + pos = (11, 6.5, 0) + Box( + position=(pos[0]-0.5, pos[1]-5.5, pos[2]+i), + texture='powerupPunch' + ) + Box( + position=(pos[0]-0.5, pos[1]-3, pos[2]+i), + texture='powerupPunch' + ) + Box( + position=(pos[0]-0.5, pos[1]-0.5, pos[2]+i), + texture='powerupPunch' + ) + pos = (-11, 6.5, 0) + Box( + position=(pos[0]+0.5, pos[1]-5.5, pos[2]+i), + texture='powerupIceBombs' + ) + Box( + position=(pos[0]+0.5, pos[1]-3, pos[2]+i), + texture='powerupIceBombs' + ) + Box( + position=(pos[0]+0.5, pos[1]-0.5, pos[2]+i), + texture='powerupIceBombs' + ) + + def spawn_player(self, player: Player) -> bs.Actor: + position = self.get_position(player) + name = player.getname() + display_color = _babase.safecolor(player.color, target_intensity=0.75) + actor = NewPlayerSpaz( + color=player.color, + highlight=player.highlight, + character=player.character, + player=player + ) + player.actor = actor + + player.actor.node.name = name + player.actor.node.name_color = display_color + player.actor.bomb_type_default = 'banana' + player.actor.bomb_type = 'banana' + + actor.connect_controls_to_player(enable_punch=True, + enable_bomb=self._bomb, + enable_pickup=True) + actor.node.hockey = True + actor.hitpoints_max = 100000 + actor.hitpoints = 100000 + actor.equip_boxing_gloves() + if self._shield: + actor.equip_shields() + actor.shield.color = (0, 0, 0) + actor.shield.radius = 0.1 + actor.shield_hitpoints = actor.shield_hitpoints_max = 100000 + + # Move to the stand position and add a flash of light. + actor.handlemessage( + StandMessage( + position, + random.uniform(0, 360))) + bs.getsound('spawn').play(volume=0.6) + return actor + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_ball_player_collide(self) -> None: + collision = bs.getcollision() + try: + ball = collision.sourcenode.getdelegate(Ball, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, True).getplayer(Player, True) + except bs.NotFoundError: + return + + ball.last_players_to_touch[player.team.id] = player + + def _kill_ball(self) -> None: + self._ball = None + + def _reset_count(self) -> None: + """reset counter of ball.""" + + assert self._ball is not None + + if self._ball.scored: + return + + bs.getsound('laser').play() + self._ball._count = self._bomb_timer + self._ball._counter.text = str(self._bomb_timer) + self._ball._tick_timer = bs.Timer( + 1.0, + call=bs.WeakCall(self._ball._tick), + repeat=True + ) + self._ball._animate = bs.animate( + self._ball.node, + 'mesh_scale', + { + 0: self._ball.node.mesh_scale, + 0.1: self._ball.scale + } + ) + if self._ball.light.color[0] == 0: + self._ball.light.color = (2, 0, 0) + else: + self._ball.light.color = (0, 0, 3) + + def update_ball(self): + if not self._ball: + return + if not self._ball.node: + return + gnode = bs.getactivity().globalsnode + + if self._ball.node.position[0] > 0: + self._ball.node.color_texture = bs.gettexture('powerupIceBombs') + bs.animate_array(gnode, 'vignette_outer', 3, {1.0: (0.4, 0.4, 0.9)}) + self._ball.color_l = (0, 0, 3.5) + self._ball._counter.color = (0, 0, 5) + else: + self._ball.node.color_texture = bs.gettexture('powerupPunch') + bs.animate_array(gnode, 'vignette_outer', 3, {1.0: (0.6, 0.45, 0.45)}) + self._ball.color_l = (2.5, 0, 0) + self._ball._counter.color = (1.2, 0, 0) + + def _handle_score(self, index=0) -> None: + """A point has been scored.""" + + assert self._ball is not None + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + if index == 0: + self.last_point = 0 + else: + self.last_point = 1 + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._ball.last_players_to_touch + and self._ball.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._ball.last_players_to_touch[scoring_team.id], + 100, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + elif team.id != index: + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.DieMessage()) + + self._foghorn_sound.play() + self._cheer_sound.play() + + bs.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, bs.PlayerDiedMessage): + + player = msg.getplayer(Player) + spaz = player.actor + spaz.node.color = (-1, -1, -1) + spaz.node.color_mask_texture = bs.gettexture('bonesColorMask') + spaz.node.color_texture = bs.gettexture('bonesColor') + spaz.node.head_mesh = bs.getmesh('bonesHead') + spaz.node.hand_mesh = bs.getmesh('bonesHand') + spaz.node.torso_mesh = bs.getmesh('bonesTorso') + spaz.node.pelvis_mesh = bs.getmesh('bonesPelvis') + spaz.node.upper_arm_mesh = bs.getmesh('bonesUpperArm') + spaz.node.forearm_mesh = bs.getmesh('bonesForeArm') + spaz.node.upper_leg_mesh = bs.getmesh('bonesUpperLeg') + spaz.node.lower_leg_mesh = bs.getmesh('bonesLowerLeg') + spaz.node.toes_mesh = bs.getmesh('bonesToes') + spaz.node.style = 'bones' + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead balls. + elif isinstance(msg, BallDiedMessage): + if not self.has_ended(): + try: + if self._ball._count == 1: + bs.timer(3.0, self._spawn_ball) + except Exception: + return + else: + super().handlemessage(msg) + + def _flash_ball_spawn(self, pos, color=(1, 0, 0)) -> None: + light = bs.newnode('light', + attrs={ + 'position': pos, + 'height_attenuated': False, + 'color': color + }) + bs.animate(light, 'intensity', {0.0: 0, 0.25: 0.2, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) + + def _spawn_ball(self) -> None: + timer = self._bomb_timer + self._swipsound.play() + self._whistle_sound.play() + pos = (random.choice([5, -5]), 2, 0) + if self.last_point != None: + if self.last_point == 0: + pos = (-5, 2, 0) + else: + pos = (5, 2, 0) + + color = (0, 0, 1*2) if pos[0] == 5 else (1*1.5, 0, 0) + texture = 'powerupPunch' if pos[0] == -5 else 'powerupIceBombs' + counter_color = (1, 0, 0) if pos[0] == -5 else (0, 0, 5) + # self._flash_ball_spawn(pos,color) + self._ball = Ball(position=pos, timer=timer, d_time=self.damage_time, color=color) + self._ball.node.color_texture = bs.gettexture(texture) + self._ball._counter.color = counter_color + + def get_position(self, player: Player) -> bs.Actor: + position = (0, 1, 0) + team = player.team.id + if team == 0: + position = (random.randint(-7, -3), 0.25, random.randint(-5, 5)) + angle = 90 + else: + position = (random.randint(3, 7), 0.25, random.randint(-5, 5)) + angle = 270 + return position + + def respawn_player(self, + player: PlayerType, + respawn_time: Optional[float] = None) -> None: + from babase._general import WeakCall + + assert player + if respawn_time is None: + respawn_time = 3.0 + + # If this standard setting is present, factor it in. + if 'Respawn Times' in self.settings_raw: + respawn_time *= self.settings_raw['Respawn Times'] + + # We want whole seconds. + assert respawn_time is not None + respawn_time = round(max(1.0, respawn_time), 0) + + if player.actor and not self.has_ended(): + from bascenev1lib.actor.respawnicon import RespawnIcon + player.customdata['respawn_timer'] = _bs.Timer( + respawn_time, WeakCall(self.spawn_player_if_exists, player)) + player.customdata['respawn_icon'] = RespawnIcon( + player, respawn_time) + + def spawn_player_if_exists(self, player: PlayerType) -> None: + """ + A utility method which calls self.spawn_player() *only* if the + bs.Player provided still exists; handy for use in timers and whatnot. + + There is no need to override this; just override spawn_player(). + """ + if player: + self.spawn_player(player) + + def spawn_player_spaz(self, player: PlayerType) -> None: + position = (0, 1, 0) + angle = None + team = player.team.id + if team == 0: + position = (random.randint(-7, -3), 0.25, random.randint(-5, 5)) + angle = 90 + else: + position = (random.randint(3, 7), 0.25, random.randint(-5, 5)) + angle = 270 + + return super().spawn_player_spaz(player, position, angle) + +##### New-Bomb##### + + +class ExplodeMessage: + """Tells an object to explode.""" + + +class ImpactMessage: + """Tell an object it touched something.""" + + +class NewBomb(bs.Actor): + + def __init__(self, position: Sequence[float] = (0, 1, 0), + velocity: Sequence[float] = (0, 0, 0), + bomb_type: str = '', + radius: float = 2.0, + source_player: bs.Player = None, + owner: bs.Node = None): + + super().__init__() + + shared = SharedObjects.get() + # Material for powerups. + self.bomb_material = bs.Material() + self.explode_material = bs.Material() + + self.bomb_material.add_actions( + conditions=( + ('we_are_older_than', 200), + 'and', + ('they_are_older_than', 200), + 'and', + ('eval_colliding', ), + 'and', + ( + ('they_have_material', shared.footing_material), + 'or', + ('they_have_material', shared.object_material), + ), + ), + actions=('message', 'our_node', 'at_connect', ImpactMessage())) + + self.explode_material.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=(('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._touch_player))) + + self._source_player = source_player + self.owner = owner + self.bomb_type = bomb_type + self.radius = radius + + owner_color = self.owner.source_player._team.color + + if self.bomb_type == 'banana': + self.node: bs.Node = bs.newnode('prop', delegate=self, attrs={ + 'position': position, + 'velocity': velocity, + 'color_texture': bs.gettexture('powerupBomb'), + 'mesh': bs.getmesh('penguinTorso'), + 'mesh_scale': 0.7, + 'body_scale': 0.7, + 'density': 3, + 'reflection': 'soft', + 'reflection_scale': [1.0], + 'shadow_size': 0.3, + 'body': 'sphere', + 'owner': owner, + 'materials': (shared.object_material, self.bomb_material)}) + + bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1, 0.26: 0.7}) + self.light = bs.newnode('light', owner=self.node, attrs={ + 'color': owner_color, + 'volume_intensity_scale': 2.0, + 'intensity': 1, + 'radius': 0.1}) + self.node.connectattr('position', self.light, 'position') + + self.spawn: bs.Timer = bs.Timer( + 10.0, self._check, repeat=True) + + def _impact(self) -> None: + node = bs.getcollision().opposingnode + node_delegate = node.getdelegate(object) + if node: + if (node is self.owner): + return + self.handlemessage(ExplodeMessage()) + + def _explode(self): + if self.node: + # Set our position a bit lower so we throw more things upward. + + pos = self.node.position + rmats = (self.explode_material,) + self.explode_region = bs.newnode( + 'region', + delegate=self, + attrs={ + 'position': (pos[0], pos[1] - 0.1, pos[2]), + 'scale': (self.radius, self.radius, self.radius), + 'type': 'sphere', + 'materials': rmats + }, + ) + if self.bomb_type == 'banana': + bs.getsound('stickyImpact').play(volume=0.35) + a = bs.emitfx(position=self.node.position, + velocity=(0, 1, 0), + count=15, + scale=1.0, + spread=0.1, + chunk_type='spark') + scorch = bs.newnode('scorch', + attrs={ + 'position': self.node.position, + 'size': 1.0, + 'big': False, + 'color': (1, 1, 0) + }) + + bs.animate(scorch, 'size', {0: 1.0, 5: 0}) + bs.timer(5, scorch.delete) + + bs.timer(0.05, self.explode_region.delete) + bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage())) + + def _touch_player(self): + node = bs.getcollision().opposingnode + collision = bs.getcollision() + try: + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + if self.bomb_type == 'banana': + color = player.team.color + owner_team = self.owner.source_player._team + if (node is self.owner): + return + if player.team == owner_team: + return + player.actor.node.handlemessage('knockout', 500.0) + bs.animate_array(player.actor.node, 'color', 3, { + 0: color, 0.1: (1.5, 1, 0), 0.5: (1.5, 1, 0), 0.6: color}) + + def _check(self) -> None: + """Prevent the cube from annihilating.""" + + def handlemessage(self, msg): + if isinstance(msg, ExplodeMessage): + self._explode() + elif isinstance(msg, ImpactMessage): + self._impact() + elif isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + elif isinstance(msg, bs.OutOfBoundsMessage): + if self.node: + self.node.delete() + +###### Object##### + + +class HealthFactory: + """Wraps up media and other resources used by bs.Bombs. + + category: Gameplay Classes + + A single instance of this is shared between all bombs + and can be retrieved via bastd.actor.bomb.get_factory(). + + Attributes: + + health_mesh + The bs.mesh of a standard health. + + health_tex + The bs.Texture for health. + + activate_sound + A bs.Sound for an activating ??. + + health_material + A bs.Material applied to health. + """ + + _STORENAME = bs.storagename() + + @classmethod + def get(cls) -> HealthFactory: + """Get/create a shared EggFactory object.""" + activity = bs.getactivity() + factory = activity.customdata.get(cls._STORENAME) + if factory is None: + factory = HealthFactory() + activity.customdata[cls._STORENAME] = factory + assert isinstance(factory, HealthFactory) + return factory + + def __init__(self) -> None: + """Instantiate a BombFactory. + + You shouldn't need to do this; call get_factory() + to get a shared instance. + """ + shared = SharedObjects.get() + + self.health_mesh = bs.getmesh('egg') + + self.health_tex = bs.gettexture('eggTex1') + + self.health_sound = bs.getsound('activateBeep') + + # Set up our material so new bombs don't collide with objects + # that they are initially overlapping. + self.health_material = bs.Material() + + self.health_material.add_actions( + conditions=( + ( + ('we_are_younger_than', 100), + 'or', + ('they_are_younger_than', 100), + ), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + + # We want pickup materials to always hit us even if we're currently + # not colliding with their node. (generally due to the above rule) + self.health_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=('modify_part_collision', 'use_node_collide', False), + ) + + self.health_material.add_actions(actions=('modify_part_collision', + 'friction', 0.3)) + + +class HealthBox(bs.Actor): + + def __init__(self, position: Sequence[float] = (0, 1, 0), + velocity: Sequence[float] = (0, 0, 0), + texture: str = 'powerupHealth'): + super().__init__() + + shared = SharedObjects.get() + factory = HealthFactory.get() + self.healthbox_material = bs.Material() + self.healthbox_material.add_actions( + conditions=( + 'they_are_different_node_than_us', + ), + actions=( + ('modify_part_collision', 'collide', True) + ) + ) + self.node: bs.Node = bs.newnode('prop', delegate=self, attrs={ + 'position': position, + 'velocity': velocity, + 'color_texture': bs.gettexture(texture), + 'mesh': bs.getmesh('powerup'), + 'light_mesh': bs.getmesh('powerupSimple'), + 'mesh_scale': 1, + 'body': 'crate', + 'body_scale': 1, + 'density': 1, + 'damping': 0, + 'gravity_scale': 1, + 'reflection': 'powerup', + 'reflection_scale': [0.5], + 'shadow_size': 0.0, + 'materials': (shared.object_material, self.healthbox_material, factory.health_material)}) + + self.light = bs.newnode('light', owner=self.node, attrs={ + 'color': (1, 1, 1), + 'volume_intensity_scale': 0.4, + 'intensity': 0.7, + 'radius': 0.0}) + self.node.connectattr('position', self.light, 'position') + + self.spawn: bs.Timer = bs.Timer( + 10.0, self._check, repeat=True) + + def _check(self) -> None: + """Prevent the cube from annihilating.""" + + def handlemessage(self, msg): + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, bs.OutOfBoundsMessage): + if self.node: + self.node.delete() + elif isinstance(msg, bs.HitMessage): + try: + spaz = msg._source_player + spaz.actor.node.handlemessage(bs.PowerupMessage(poweruptype='health')) + t_color = spaz.team.color + spaz.actor.node.color = t_color + bs.getsound('healthPowerup').play(volume=0.5) + bs.animate(self.light, 'radius', {0: 0.0, 0.1: 0.2, 0.7: 0}) + except: + pass + + elif isinstance(msg, bs.DroppedMessage): + spaz = msg.node.getdelegate(PlayerSpaz) + self.regen_timer = None + + +class Torso(bs.Actor): + + def __init__(self, position: Sequence[float] = (0, 1, 0), + velocity: Sequence[float] = (0, 0, 0), + texture: str = 'bonesColor'): + super().__init__() + + shared = SharedObjects.get() + + self.node: bs.Node = bs.newnode('prop', delegate=self, attrs={ + 'position': position, + 'velocity': velocity, + 'color_texture': bs.gettexture(texture), + 'mesh': bs.getmesh('bonesTorso'), + 'mesh_scale': 1, + 'body': 'sphere', + 'body_scale': 0.5, + 'density': 6, + 'damping': 0, + 'gravity_scale': 1, + 'reflection': 'soft', + 'reflection_scale': [0], + 'shadow_size': 0.0, + 'materials': (shared.object_material,)}) + + self.spawn: bs.Timer = bs.Timer( + 10.0, self._check, repeat=True) + + def _check(self) -> None: + """Prevent the cube from annihilating.""" + + def handlemessage(self, msg): + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, bs.OutOfBoundsMessage): + if self.node: + self.node.delete() + + +class Bone(bs.Actor): + + def __init__(self, position: Sequence[float] = (0, 1, 0), + velocity: Sequence[float] = (0, 0, 0), + texture: str = 'bonesColor', + style: int = 0): + super().__init__() + + shared = SharedObjects.get() + meshs = ['bonesUpperArm', 'bonesUpperLeg', 'bonesForeArm', + 'bonesPelvis', 'bonesToes', 'bonesHand'] + bone = None + mesh = 0 + for i in meshs: + if mesh == style: + bone = meshs[mesh] + else: + mesh += 1 + self.node: bs.Node = bs.newnode('prop', delegate=self, attrs={ + 'position': position, + 'velocity': velocity, + 'color_texture': bs.gettexture(texture), + 'mesh': bs.getmesh(bone), + 'mesh_scale': 1.5, + 'body': 'crate', + 'body_scale': 0.6, + 'density': 2, + 'damping': 0, + 'gravity_scale': 1, + 'reflection': 'soft', + 'reflection_scale': [0], + 'shadow_size': 0.0, + 'materials': (shared.object_material,)}) + + self.spawn: bs.Timer = bs.Timer( + 10.0, self._check, repeat=True) + + def _check(self) -> None: + """Prevent the cube from annihilating.""" + + def handlemessage(self, msg): + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, bs.OutOfBoundsMessage): + if self.node: + self.node.delete() + +###### Object##### + + +class Box(bs.Actor): + + def __init__(self, position: Sequence[float] = (0, 1, 0), + velocity: Sequence[float] = (0, 0, 0), + texture: str = 'powerupCurse'): + super().__init__() + + shared = SharedObjects.get() + self.dont_collide = bs.Material() + self.dont_collide.add_actions( + conditions=( + 'they_are_different_node_than_us', + ), + actions=( + ('modify_part_collision', 'collide', False) + ) + ) + + self.node: bs.Node = bs.newnode('prop', delegate=self, attrs={ + 'position': position, + 'velocity': velocity, + 'color_texture': bs.gettexture(texture), + 'mesh': bs.getmesh('powerup'), + 'light_mesh': bs.getmesh('powerupSimple'), + 'mesh_scale': 4, + 'body': 'box', + 'body_scale': 3, + 'density': 9999, + 'damping': 9999, + 'gravity_scale': 0, + 'reflection': 'soft', + 'reflection_scale': [0.25], + 'shadow_size': 0.0, + 'materials': [self.dont_collide,]}) diff --git a/plugins/minigames/hot_potato.py b/plugins/minigames/hot_potato.py new file mode 100644 index 000000000..af804020a --- /dev/null +++ b/plugins/minigames/hot_potato.py @@ -0,0 +1,1414 @@ +""" + + Hot Potato by themikirog + Version 3 + + A random player(s) gets Marked. + They will die if they don't pass the mark to other players. + After they die, another random player gets Marked. + Last player standing wins! + + Heavily commented for easy modding learning! + + No Rights Reserved + +""" + +# ba_meta require api 9 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING, override + +# Define only what we need and nothing more +import babase +import bascenev1 as ba +from bascenev1lib.actor.spaz import SpazFactory +from bascenev1lib.actor.spaz import PickupMessage +from bascenev1lib.actor.spaz import BombDiedMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.bomb import Bomb, Blast, ExplodeMessage +from bascenev1lib.gameutils import SharedObjects +from enum import Enum +import random + +IMPACT_BOMB_RADIUS_SCALE = 0.7 +MARKED_KNOCKBACK_SCALE = 0.8 + +if TYPE_CHECKING: + pass + +# Let's define stun times for falling. +# First element is stun for the first fall, second element is stun for the second fall and so on. +# If we fall more than the amount of elements on this list, we'll use the last entry. +FALL_PENALTIES = [1.5, + 2.5, + 3.5, + 5.0, + 6.0, + 7.0, + 8.0, + 9.0, + 10.0] + +# Added a different fall penalty table if playing with lenient penalties. +LENIENT_FALL_PENALTIES = [1.5, + 2.0, + 2.5, + 3.0, + 3.5, + 4.0, + 4.5, + 5.0, + 5.5, + 6.0, + 7.0, + 8.0, + 9.0, + 10.0] + +RED_COLOR = (1.0, 0.2, 0.2) +YELLOW_COLOR = (1.0, 1.0, 0.2) + + +# The player in Hot Potato can be in one of these states: +class PlayerState(Enum): + # REGULAR - the state all players start in. + REGULAR = 0 + # MARKED - when a player is marked, they'll be eliminated when the timer hits zero. + # Marked players pass the mark to REGULAR or STUNNED players by harming or grabbing other players. + # MARKED players respawn instantly if they somehow get knocked off the map. + MARKED = 1 + # ELIMINATED - a player is eliminated if the timer runs out during the MARKED state or they leave the game. + # These players can't win and won't respawn. + ELIMINATED = 2 + # STUNNED - if a REGULAR player falls out of the map, they'll receive the STUNNED state. + # STUNNED players are incapable of all movement and actions. + # STUNNED players can still get MARKED, but can't be punched, grabbed or knocked around by REGULAR players. + # STUNNED players will go back to the REGULAR state after several seconds. + # The time it takes to go back to the REGULAR state gets more severe the more times the player dies by falling off the map. + STUNNED = 3 + +# I added some variety by giving Marked player unique bomb types. +# This enum makes it easier to keep track of them. + + +class MarkedBombTypes(Enum): + REGULAR = 0 + IMPACT = 1 + STICKY = 2 + +# To make the game easier to parse, I added Elimination style icons to the bottom of the screen. +# Here's the behavior of each icon. + + +class Icon(ba.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: tuple[float, float], + scale: float, + name_scale: float = 1.0, + name_maxwidth: float = 100.0, + shadow: float = 1.0): + super().__init__() + + # Define the player this icon belongs to + self._player = player + self._name_scale = name_scale + + self._outline_tex = ba.gettexture('characterIconMask') + + # Character portrait + icon = player.get_icon() + self.node = ba.newnode('image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + # Player name + self._name_text = ba.newnode( + 'text', + owner=self.node, + attrs={ + 'text': ba.Lstr(value=player.getname()), + 'color': ba.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + # Status text (such as Marked!, Stunned! and You're Out!) + self._marked_text = ba.newnode( + 'text', + owner=self.node, + attrs={ + 'text': '', + 'color': (1, 0.1, 0.0), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + # Status icon overlaying the character portrait + self._marked_icon = ba.newnode( + 'text', + owner=self.node, + attrs={ + 'text': babase.charstr(babase.SpecialChar.HAL), + 'color': (1, 1, 1), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 430, + 'shadow': 0.0, + 'opacity': 0.0, + 'flatness': 1.0, + 'scale': 2.1, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_marked_icon(player.state) + self.set_position_and_scale(position, scale) + + # Change our icon's appearance depending on the player state. + def set_marked_icon(self, type: PlayerState) -> None: + pos = self.node.position + # Regular players get no icons or status text + if type is PlayerState.REGULAR: + self._marked_icon.text = '' + self._marked_text.text = '' + self._marked_icon.opacity = 0.0 + self._name_text.flatness = 1.0 + assert self.node + self.node.color = (1.0, 1.0, 1.0) + # Marked players get ALL of the attention - red portrait, red text and icon overlaying the portrait + elif type is PlayerState.MARKED: + self._marked_icon.text = babase.charstr(babase.SpecialChar.HAL) + self._marked_icon.position = (pos[0] - 1, pos[1] - 13) + self._marked_icon.opacity = 1.0 + self._marked_text.text = 'Marked!' + self._marked_text.color = (1.0, 0.0, 0.0) + self._name_text.flatness = 0.0 + assert self.node + self.node.color = (1.0, 0.2, 0.2) + # Stunned players are just as important - yellow portrait, yellow text and moon icon. + elif type is PlayerState.STUNNED: + self._marked_icon.text = babase.charstr(babase.SpecialChar.MOON) + self._marked_icon.position = (pos[0] - 2, pos[1] - 12) + self._marked_icon.opacity = 1.0 + self._marked_text.text = 'Stunned!' + self._marked_text.color = (1.0, 1.0, 0.0) + assert self.node + self.node.color = (0.75, 0.75, 0.0) + # Eliminated players get special treatment. + # We make the portrait semi-transparent, while adding some visual flair with an fading skull icon and text. + elif type is PlayerState.ELIMINATED: + self._marked_icon.text = babase.charstr(babase.SpecialChar.SKULL) + self._marked_icon.position = (pos[0] - 2, pos[1] - 12) + self._marked_text.text = 'You\'re Out!' + self._marked_text.color = (0.5, 0.5, 0.5) + + # Animate text and icon + animation_end_time = 1.5 if bool(self.activity.settings['Epic Mode']) else 3.0 + ba.animate(self._marked_icon, 'opacity', { + 0: 1.0, + animation_end_time: 0.0}) + ba.animate(self._marked_text, 'opacity', { + 0: 1.0, + animation_end_time: 0.0}) + + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + else: + # If we beef something up, let the game know we made a mess in the code by providing a non-existant state. + raise Exception("invalid PlayerState type") + + # Set where our icon is positioned on the screen and how big it is. + def set_position_and_scale(self, position: tuple[float, float], + scale: float) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + self._marked_text.position = (position[0], position[1] - scale * 52.0) + self._marked_text.scale = 0.8 * scale + +# Bombs work as intended... with the exception of one mechanic. +# We have to patch it out. + + +class PotatoBomb(Bomb): + + # Same function as before with bomb ownership passing code removed + @override + def _handle_hit(self, msg: ba.HitMessage) -> None: + ispunched = msg.srcnode and msg.srcnode.getnodetype() == 'spaz' + + # Normal bombs are triggered by non-punch impacts; + # impact-bombs by all impacts. + if not self._exploded and ( + not ispunched or self.bomb_type in ['impact', 'land_mine'] + ): + # Here lies a mechanic where bomb ownership passed with explosions. + # You won't be missed. RIP + + ba.timer( + 0.1 + random.random() * 0.1, + ba.WeakCall(self.handlemessage, ExplodeMessage()), + ) + assert self.node + self.node.handlemessage( + 'impulse', + msg.pos[0], + msg.pos[1], + msg.pos[2], + msg.velocity[0], + msg.velocity[1], + msg.velocity[2], + msg.magnitude, + msg.velocity_magnitude, + msg.radius, + 0, + msg.velocity[0], + msg.velocity[1], + msg.velocity[2], + ) + + if msg.srcnode: + pass + + +class FootConnectMessage(object): + 'Player stands on ground' + pass + + +class FootDisconnectMessage(object): + 'Player stops touching the ground' + pass + + +# This gamemode heavily relies on edited player behavior. +# We need that amount of control, so we're gonna create our own class and use the original PlayerSpaz as our blueprint. + + +class PotatoPlayerSpaz(PlayerSpaz): + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) # unchanged Spaz __init__ code goes here + shared = SharedObjects.get() + self._touched_ground_count = 0 + self.dropped_bombs = [] # we use this to track bombs thrown by the player + + # Immediately turn off invincibility unless you're marked + self.node.invincible = (self.source_player.state == PlayerState.MARKED) + + footing_material = shared.footing_material + self.potato_material = ba.Material() + self.potato_material.add_actions( + conditions=('they_have_material', footing_material), + actions=( + ('message', 'our_node', 'at_connect', FootConnectMessage()), + ('message', 'our_node', 'at_disconnect', FootDisconnectMessage()) + ), + ) + + new_extras = list(self.node.extras_material) + new_extras.append(self.potato_material) + self.node.extras_material = tuple(new_extras) + + # We meed to modify how grabs work, but mostly small changes to prevent infinites + # This cooldown will work differently compared to punches, which we'll see later + self._able_to_pickup = True + self._pickup_timer: ba.Timer = None + self._pickup_cooldown = 0.5 + + # Define a marked light + self.marked_light = ba.newnode('light', + owner=self.node, + attrs={'position': self.node.position, + 'radius': 0.15, + 'intensity': 0.0, + 'height_attenuated': False, + 'color': (1.0, 0.0, 0.0)}) + + # Pulsing red light when the player is Marked + ba.animate(self.marked_light, 'radius', { + 0: 0.1, + 0.3: 0.15, + 0.6: 0.1}, + loop=True) + self.node.connectattr('position_center', self.marked_light, 'position') + + # Marked timer. It should be above our head, so we attach the text to the offset that's attached to the player. + self.marked_timer_offset = ba.newnode('math', owner=self.node, attrs={ + 'input1': (0, 1.2, 0), + 'operation': 'add'}) + self.node.connectattr('torso_position', self.marked_timer_offset, 'input2') + + self.marked_timer_text = ba.newnode('text', owner=self.node, attrs={ + 'text': '', + 'in_world': True, + 'shadow': 0.4, + 'color': (RED_COLOR[0], RED_COLOR[1], RED_COLOR[2], 0.0), + 'flatness': 0, + 'scale': 0.02, + 'h_align': 'center'}) + self.marked_timer_offset.connectattr('output', self.marked_timer_text, 'position') + + # We need to modify how grabs work to prevent inifinites. + # Since it's a tiny overhaul, we need to rewrite this function entirely. + # If you're not marked, releasing someone from a grab will prevent you from grabbing for a tiny bit. + def on_pickup_press(self) -> None: + if not self.node: + return + + t_ms = int(ba.time() * 1000.0) + assert isinstance(t_ms, int) + + if self._able_to_pickup: + _holding_someone: bool = self.node.hold_node.exists() + _marked = self.node.source_player.state == PlayerState.MARKED + if not _marked and _holding_someone: + self._able_to_pickup = False + self._pickup_timer = ba.Timer( + self._pickup_cooldown, ba.WeakCall(self._on_pickup_timer_timeout)) + self.node.pickup_pressed = True + self.last_pickup_time_ms = t_ms + + self._turbo_filter_add_press('pickup') + + # If grab timer runs out, restore our ability to grab. + def _on_pickup_timer_timeout(self) -> None: + self._able_to_pickup = True + + # This function is based on the original drop_bomb function. + # What we wanna do here is spawn our own bomb instead of the original. + def create_bomb(self) -> stdbomb.Bomb | None: + if (self.bomb_count <= 0) or self.frozen: + return None + assert self.node + pos = self.node.position_forward + vel = self.node.velocity + + dropping_bomb = True + bomb_type = self.bomb_type + + bomb = PotatoBomb( + position=(pos[0], pos[1] - 0.0, pos[2]), + velocity=(vel[0], vel[1], vel[2]), + bomb_type=bomb_type, + blast_radius=self.blast_radius, + source_player=self.source_player, + owner=self.node, + ).autoretain() + + assert bomb.node + if dropping_bomb: + self.bomb_count -= 1 + bomb.node.add_death_action( + ba.WeakCall(self.handlemessage, BombDiedMessage()) + ) + self._pick_up(bomb.node) + + for clb in self._dropped_bomb_callbacks: + clb(self, bomb) + + return bomb + + # Modified behavior when dropping bombs + def drop_bomb(self) -> stdbomb.Bomb | None: + # The original function returns the Bomb the player created. + # This is super helpful for us, since all we need here is to lightly tweak the bomb. + bomb = self.create_bomb() + # Let's make sure the player actually created a new bomb + if bomb: + # Keep track of who created the bomb in the first place, since this is crucial + self.creator_player = self.source_player + + # Add our bomb to the list of our tracked bombs + self.dropped_bombs.append(bomb) + # Bring a light + bomb.bomb_marked_light = ba.newnode('light', + owner=bomb.node, + attrs={'position': bomb.node.position, + 'radius': 0.04, + 'intensity': 0.0, + 'height_attenuated': False, + 'color': (1.0, 0.0, 0.0)}) + # Attach the light to the bomb + bomb.node.connectattr('position', bomb.bomb_marked_light, 'position') + # Let's adjust all lights for all bombs that we own. + self.set_bombs_marked() + # When the bomb physics node dies, call a function. + bomb.node.add_death_action( + ba.WeakCall(self.bomb_died, bomb)) + + # Here's the function that gets called when one of the player's bombs dies. + # We reference the player's dropped_bombs list and remove the bomb that died. + + def bomb_died(self, bomb): + self.dropped_bombs.remove(bomb) + + # Go through all the bombs this player has in the world. + # Paint them red if the owner is marked, turn off the light otherwise. + # We need this light to inform the player about bombs YOU DON'T want to get hit by. + def set_bombs_marked(self): + for bomb in self.dropped_bombs: + bomb.bomb_marked_light.intensity = 20.0 if self._player.state == PlayerState.MARKED else 0.0 + + def handlemessage(self, msg): + # Since our gamemode relies heavily on players passing the mark to other players + # we need to have access to this message. This gets called when the player takes damage for any reason. + if isinstance(msg, ba.HitMessage): + # This is basically the same HitMessage code as in the original Spaz. + # The only difference is that there is no health bar and you can't die with punches or bombs. + # On top of that we also have to fix gamemode breaking exploits related to this change in gameplay. + # Also some useless or redundant code was removed. + # I'm still gonna comment all of it since we're here. + if not self.node: + return None + if self.node.invincible: + SpazFactory.get().block_sound.play( + 1.0, + position=self.node.position, + ) + return True + + # Here's all the damage and force calculations unchanged from the source. + mag = msg.magnitude * self.impact_scale + velocity_mag = msg.velocity_magnitude * self.impact_scale + damage_scale = 0.22 + + # If we're marked, decrease the stun so its less punishing. + victim_marked: bool = False + if hasattr(self.node.source_player, "state"): + if self.node.source_player.state == PlayerState.MARKED: + victim_marked = True + mag *= MARKED_KNOCKBACK_SCALE + velocity_mag *= MARKED_KNOCKBACK_SCALE + + # If the attacker is marked, pass that mark to us. + self.activity.pass_mark(msg._source_player, self._player) + + # When stun timer runs out, we explode. Let's make sure our own explosion does throw us around. + if msg.hit_type == 'stun_blast' and msg._source_player == self.source_player: + return True + # If the attacker is healthy and we're stunned, do a flash and play a sound, then ignore the rest of the code. + if self.source_player.state == PlayerState.STUNNED and msg._source_player != PlayerState.MARKED: + self.node.handlemessage('flash') + SpazFactory.get().block_sound.play(1, position=self.node.position) + return True + + # We use them to apply a physical force to the player. + # Normally this is also used for damage, but we we're not gonna do it. + # We're still gonna calculate it, because it's still responsible for knockback. + # However knockback is also stun, which can be problematic in this mode where you can't die. + # If a player is already stunned, let's disable all knockback. + use_knockback = not self.node.knockout > 0.0 + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], mag, + velocity_mag, msg.radius, not use_knockback, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + damage = int(damage_scale * self.node.damage) + + # We're multiplying damage after applying knockback so that it still feels like we did a lot of damage. + # This new damage value influences particles and sound, so we gotta keep it the same. + if victim_marked: + damage /= MARKED_KNOCKBACK_SCALE + + self.node.handlemessage('hurt_sound') # That's how we play spaz node's hurt sound + + # Play punch impact sounds based on damage if it was a punch. + # We don't show damage percentages, because it's irrelevant. + if msg.hit_type == 'punch': + self.on_punched(damage) + + if damage >= 500: + sounds = SpazFactory.get().punch_sound_strong + sound = sounds[random.randrange(len(sounds))] + elif damage >= 100: + sound = SpazFactory.get().punch_sound + else: + sound = SpazFactory.get().punch_sound_weak + sound.play(1.0, position=self.node.position) + + # Throw up some chunks. + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 0.5, + msg.force_direction[1] * 0.5, + msg.force_direction[2] * 0.5), + count=min(10, 1 + int(damage * 0.0025)), + scale=0.3, + spread=0.03) + + ba.emitfx(position=msg.pos, + chunk_type='sweat', + velocity=(msg.force_direction[0] * 1.3, + msg.force_direction[1] * 1.3 + 5.0, + msg.force_direction[2] * 1.3), + count=min(30, 1 + int(damage * 0.04)), + scale=0.9, + spread=0.28) + + # Momentary flash. This spawns around where the Spaz's punch would be (we're kind of guessing here). + hurtiness = damage * 0.003 + punchpos = (msg.pos[0] + msg.force_direction[0] * 0.02, + msg.pos[1] + msg.force_direction[1] * 0.02, + msg.pos[2] + msg.force_direction[2] * 0.02) + flash_color = (1.0, 0.8, 0.4) + light = ba.newnode( + 'light', + attrs={ + 'position': punchpos, + 'radius': 0.12 + hurtiness * 0.12, + 'intensity': 0.3 * (1.0 + 1.0 * hurtiness), + 'height_attenuated': False, + 'color': flash_color + }) + ba.timer(0.06, light.delete) + + flash = ba.newnode('flash', + attrs={ + 'position': punchpos, + 'size': 0.17 + 0.17 * hurtiness, + 'color': flash_color + }) + ba.timer(0.06, flash.delete) + + # Physics collision particles. + if msg.hit_type == 'impact': + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 2.0, + msg.force_direction[1] * 2.0, + msg.force_direction[2] * 2.0), + count=min(10, 1 + int(damage * 0.01)), + scale=0.4, + spread=0.1) + + # Briefly flash when hit. + # We shouldn't do this if we're dead. + if self.hitpoints > 0: + + self.node.handlemessage('flash') + + # If we're holding something, drop it. + if damage > 0.0 and self.node.hold_node: + self.node.hold_node = None + # If we get grabbed, this function is called. + # We want to pass along the mark with grabs too. + elif isinstance(msg, PickupMessage): + # Make sure our body exists. + if not self.node: + return None + + # Let's get all collision data if we can. Otherwise cancel. + try: + collision = ba.getcollision() + opposingnode = collision.opposingnode + except ba.NotFoundError: + return True + + # Don't allow picking up of invincible dudes. + try: + if opposingnode.invincible: + return True + except Exception: + pass + + # Our grabber needs to be a Spaz + if opposingnode.getnodetype() == 'spaz': + # Disallow grabbing if a healthy player tries to grab us and we're stunned. + # If they're marked, continue with our scheduled program. + # It's the same sound and flashing behavior as hitting a stunned player as a healthy player. + if hasattr(opposingnode.source_player, "state"): + if (opposingnode.source_player.state == PlayerState.STUNNED and self.source_player.state != PlayerState.MARKED): + opposingnode.handlemessage('flash') + SpazFactory.get().block_sound.play( + 1.0, + position=self.node.position, + ) + return True + # If they're marked and we're healthy or stunned, pass that mark along to us. + elif opposingnode.source_player.state in [PlayerState.REGULAR, PlayerState.STUNNED] and self.source_player.state == PlayerState.MARKED: + self.activity.pass_mark(self.source_player, opposingnode.source_player) + + # Our work is done. Continue with the rest of the grabbing behavior as usual. + super().handlemessage(msg) + # Called if we touch the ground thanks to an extra material we added to the Spaz. + elif isinstance(msg, FootConnectMessage): + self._touched_ground_count += 1 + # If we are stunned but the time is at 0, only remove stun if we land. + if self.source_player: + if self._touched_ground_count > 0 and self.source_player.stunned_time_remaining <= 0.0: + self.source_player.stun_remove() + # Called if we stop touching the ground thanks to an extra material we added to the Spaz. + elif isinstance(msg, FootDisconnectMessage): + self._touched_ground_count -= 1 + # Dying is important in this gamemode and as such we need to address this behavior. + elif isinstance(msg, ba.DieMessage): + + # If a player left the game, inform our gamemode logic. + if msg.how == ba.DeathType.LEFT_GAME: + self.activity.player_left(self.source_player) + + # If a MARKED or STUNNED player dies, hide the text from the previous spaz. + if self.source_player.state in [PlayerState.MARKED, PlayerState.STUNNED]: + self.marked_timer_text.color = (self.marked_timer_text.color[0], + self.marked_timer_text.color[1], + self.marked_timer_text.color[2], + 0.0) + ba.animate(self.marked_light, 'intensity', { + 0: self.marked_light.intensity, + 0.5: 0.0}) + + # Continue with the rest of the behavior. + super().handlemessage(msg) + # If a message is something we haven't modified yet, let's pass it along to the original. + else: + super().handlemessage(msg) + +# A concept of a player is very useful to reference if we don't have a player character present (maybe they died). + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + # Most of these are self explanatory. + self.icon: Icon = None + self.fall_times: int = 0 + self.state: PlayerState = PlayerState.REGULAR + self.stunned_time_remaining = 0.0 + # These are references to timers responsible for handling stunned behavior. + self.stunned_timer = None + self.stunned_update_timer = None + + # If we're stunned, a timer calls this every 0.1 seconds. + def stunned_timer_tick(self) -> None: + # Decrease our time remaining then change the text displayed above the Spaz's head + self.stunned_time_remaining -= 0.1 + self.stunned_time_remaining = max(0.0, self.stunned_time_remaining) + self.actor.marked_timer_text.text = str(round(self.stunned_time_remaining, 2)) + + # When stun time is up, call this function. + def stun_remove(self) -> None: + # Let's proceed only if we're stunned + if self.state != PlayerState.STUNNED: + return + + # Don't remove stun if we're airborne + if self.actor._touched_ground_count <= 0: + return + + # Do an explosion where we're standing. Normally it would throw us around, but we dealt + # with this issue in PlayerSpaz's edited HitMessage in line 312. + Blast(position=self.actor.node.position, + velocity=self.actor.node.velocity, + blast_radius=2.5, + # This hit type allows us to ignore our own stun blast explosions. + hit_type='stun_blast', + source_player=self).autoretain() + # Let's switch our state back to healthy. + self.set_state(PlayerState.REGULAR) + + # States are a key part of this gamemode and a lot of logic has to be done to acknowledge these state changes. + def set_state(self, state: PlayerState) -> None: + # Let's remember our old state before we change it. + old_state = self.state + + # If we're stunned, disconnect our controls + if state == PlayerState.STUNNED: + self.actor.disconnect_controls_from_player() + + # If we just became stunned, do all of this: + if old_state != PlayerState.STUNNED and state == PlayerState.STUNNED: + + # Let's set our stun time based on the amount of times we fell out of the map. + fall_penalties_table = LENIENT_FALL_PENALTIES if self.actor.getactivity( + ).lenient_fall_penalties else FALL_PENALTIES + if self.fall_times < len(fall_penalties_table): + stun_time = fall_penalties_table[self.fall_times] + else: + stun_time = fall_penalties_table[len(fall_penalties_table) - 1] + + self.stunned_time_remaining = stun_time # Set our stun time remaining + # Remove our stun once the time is up + self.stunned_timer = ba.Timer(stun_time + 0.1, ba.Call(self.stun_remove)) + self.stunned_update_timer = ba.Timer(0.1, ba.Call( + self.stunned_timer_tick), repeat=True) # Call a function every 0.1 seconds + self.fall_times += 1 # Increase the amount of times we fell by one + # Change the text above the Spaz's head to total stun time + self.actor.marked_timer_text.text = str(stun_time) + + # If we were stunned, but now we're not, let's reconnect our controls. + # CODING CHALLENGE: to punch or bomb immediately after the stun ends, you need to + # time the button press frame-perfectly in order for it to work. + # What if we could press the button shortly before stun ends to do the action as soon as possible? + # If you're feeling up to the challenge, feel free to implement that! + if old_state == PlayerState.STUNNED and state != PlayerState.STUNNED: + self.actor.connect_controls_to_player() + + # When setting a state that is not STUNNED, clear all timers. + if state != PlayerState.STUNNED: + self.stunned_timer = None + self.stunned_update_timer = None + + # Here's all the light and text colors that we set depending on the state. + if state == PlayerState.MARKED: + self.actor.marked_light.intensity = 1.5 + self.actor.marked_light.color = (1.0, 0.0, 0.0) + self.actor.marked_timer_text.color = (RED_COLOR[0], + RED_COLOR[1], + RED_COLOR[2], + 1.0) + elif state == PlayerState.STUNNED: + self.actor.marked_light.intensity = 0.5 + self.actor.marked_light.color = (1.0, 1.0, 0.0) + self.actor.marked_timer_text.color = (YELLOW_COLOR[0], + YELLOW_COLOR[1], + YELLOW_COLOR[2], + 1.0) + else: + self.actor.marked_light.intensity = 0.0 + self.actor.marked_timer_text.text = '' + + self.state = state + self.actor.set_bombs_marked() # Light our bombs red if we're Marked, removes the light otherwise + self.icon.set_marked_icon(state) # Update our icon + + +# ba_meta export bascenev1.GameActivity +class HotPotato(ba.TeamGameActivity[Player, ba.Team]): + + # Let's define the basics like the name of the game, description and some tips that should appear at the start of a match. + name = 'Hot Potato' + description = ('A random player gets marked.\n' + 'Pass the mark to other players.\n' + 'Marked player gets eliminated when time runs out.\n' + 'Last one standing wins!') + tips = [ + 'You can pass the mark not only with punches and grabs, but bombs as well.', + 'If you\'re not marked, DON\'T fall off the map!\nEach fall will be punished with immobility.', + 'Falling can be a good escape strategy, but don\'t over rely on it.\nYou\'ll be defenseless if you respawn!', + 'Stunned players are immune to healthy players, but not to Marked players!', + 'Each fall when not Marked increases your time spent stunned.', + 'If two players are falling, it\'s better for the Marked player.\nIt might be worth it to get Marked just to avoid the falling penalty!', + 'If you\'re Marked and have tons of time, keep your cool and pick your targets.\nIt\'s last man that\'s standing that wins after all!', + 'Hitting knocked out players won\'t increase the knock out time.\nWait until they get up before doing it again!', + 'Grabs are instant and pass the mark.\nIf you\'re in a rush, grab the player!', + 'If you\'re Marked, go for shy and passive players.\nThey are often cornered and can\'t escape easily.', + 'Don\'t neglect your bombs! They can catch players\nthat would normally be able to get away easily!', + 'Try throwing healthy players off the map to make their timers\nlonger the next time they get stunned.', + 'Marked players don\'t get stunned when falling off the map.', + 'For total disrespect, try throwing the Marked player off the map\nwithout getting marked yourself!', + 'Feeling evil? Throw healthy players towards the Marked player!', + 'Red bombs belong to the Marked player!\nWatch out for those!', + 'Stunned players explode when their stun timer runs out.\nIf that time is close to zero, keep your distance!' + ] + + # We're gonna distribute end of match session scores based on who dies first and who survives. + # First place gets most points, then second, then third. + scoreconfig = ba.ScoreConfig(label='Place', + scoretype=ba.ScoreType.POINTS, + lower_is_better=True) + + # These variables are self explanatory too. + show_kill_points = False + allow_mid_activity_joins = False + + # Let's define some settings the user can mess around with to fit their needs. + available_settings = [ + ba.IntSetting('Elimination Timer', + min_value=5, + default=15, + increment=1, + ), + ba.IntChoiceSetting( + 'Marked Player Bomb', + choices=[ + ('Regular', MarkedBombTypes.REGULAR.value), + ('Impact', MarkedBombTypes.IMPACT.value), + ('Sticky', MarkedBombTypes.STICKY.value), + ], + default=MarkedBombTypes.REGULAR.value, + ), + ba.BoolSetting('Lenient Fall Penalties', default=False), + ba.BoolSetting('Epic Mode', default=False), + ] + + # Update the gamemode's name to differentiate between different variants. + @override + @classmethod + def get_display_string(cls, settings: dict | None = None) -> babase.Lstr: + name = ba.Lstr(translate=('gameNames', cls.getname())) + if settings is not None: + + if 'Marked Player Bomb' in settings and settings['Marked Player Bomb'] == MarkedBombTypes.IMPACT.value: + name = ba.Lstr(value='Impact ${A}', + subs=[('${A}', name)]) + elif 'Marked Player Bomb' in settings and settings['Marked Player Bomb'] == MarkedBombTypes.STICKY.value: + name = ba.Lstr(value='Sticky ${A}', + subs=[('${A}', name)]) + + if 'Epic Mode' in settings and settings['Epic Mode']: + name = babase.Lstr( + resource='epicNameFilterText', subs=[('${NAME}', name)] + ) + return name + + # Hot Potato is strictly a Free-For-All gamemode, so only picking the gamemode in FFA playlists. + @override + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.FreeForAllSession) + + # Most maps should work in Hot Potato. Generally maps marked as 'melee' are the most versatile map types of them all. + # As the name implies, fisticuffs are common forms of engagement. + @override + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + assert ba.app.classic is not None + return ba.app.classic.getmaps('melee') + + # Here we define everything the gamemode needs, like sounds and settings. + def __init__(self, settings: dict): + super().__init__(settings) + self.settings = settings + + # We set this to help with player spawning later. + self._next_ffa_start_index = 0 + + # Let's define all of the sounds we need. + self._tick_sound = ba.getsound('tick') + self._player_eliminated_sound = ba.getsound('playerDeath') + # These next sounds are arrays instead of single sounds. + # We'll use that fact later. + self._danger_tick_sounds = [ba.getsound('orchestraHit'), + ba.getsound('orchestraHit2'), + ba.getsound('orchestraHit3')] + self._marked_sounds = [ba.getsound('powerdown01'), + ba.getsound('activateBeep'), + ba.getsound('hiss')] + + # Normally play KOTH music, but switch to Epic music if we're in slow motion. + self._epic_mode = bool(settings['Epic Mode']) + self.lenient_fall_penalties = bool(settings['Lenient Fall Penalties']) + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.SCARY) + + # This description appears below the title card after it comes crashing when the game begins. + def get_instance_description(self) -> str | Sequence: + return 'Pass the mark to someone else before you explode!' + + # This is the tiny text that is displayed in the corner during the game as a quick reminder of the objective. + def get_instance_description_short(self) -> str | Sequence: + return 'pass the mark' + + # Set up our player every time they join. + # Because you can't join mid-match, this will always be called at the beginning of the game. + def on_player_join(self, player: Player) -> None: + + player.state = PlayerState.REGULAR + player.fall_times = 0 + + # Create our icon and spawn. + if not self.has_begun(): + player.icon = Icon(player, position=(0, 50), scale=0.8) + self.spawn_player(player) + + # Returns every single marked player. + # This piece of info is used excensively in this gamemode, so it's advantageous to have a function to cut on + # work and make the gamemode easier to maintain + def get_marked_players(self) -> Sequence[ba.Player]: + marked_players = [] + for p in self.players: + if p.state == PlayerState.MARKED: + marked_players.append(p) + return marked_players + + # Marks a player. This sets their state, spawns some particles and sets the timer text above their heads. + def mark(self, target: Player) -> None: + target.set_state(PlayerState.MARKED) + + ba.emitfx(position=target.actor.node.position, + velocity=target.actor.node.velocity, + chunk_type='spark', + count=int(20.0+random.random()*20), + scale=1.0, + spread=1.0) + + # Change their bomb type depending on the variant + if self.settings['Marked Player Bomb'] == MarkedBombTypes.IMPACT.value: + # Increase blast radius of impact bombs to be on par with normal bombs + target.actor.blast_radius /= IMPACT_BOMB_RADIUS_SCALE + target.actor.bomb_type = 'impact' + elif self.settings['Marked Player Bomb'] == MarkedBombTypes.STICKY.value: + target.actor.bomb_type = 'sticky' + + target.actor.marked_timer_text.text = str(self.elimination_timer_display) + + # Removes the mark from the player. This restores the player to its initial state. + def remove_mark(self, target: Player) -> None: + if target.state != PlayerState.MARKED: + return + + if self.settings['Marked Player Bomb'] == MarkedBombTypes.IMPACT.value: + target.actor.blast_radius *= IMPACT_BOMB_RADIUS_SCALE # Restore normal blast radius + target.actor.bomb_type = 'normal' + + target.set_state(PlayerState.REGULAR) + target.actor.marked_timer_text.text = '' + + # Pass the mark from one player to another. + # This is more desirable than calling mark and remove_mark functions constantly and gives us + # more control over the mark spreading mechanic. + def pass_mark(self, marked_player: Player, hit_player: Player) -> None: + # Make sure both players meet the requirements + if not marked_player or not hit_player: + return + if marked_player.state == PlayerState.MARKED and hit_player.state != PlayerState.MARKED: + self.mark(hit_player) + self.remove_mark(marked_player) + + # This function is called every second a marked player exists. + def _eliminate_tick(self) -> None: + marked_players = self.get_marked_players() + marked_player_amount = len(marked_players) + + # If there is no marked players, raise an exception. + # This is used for debugging purposes, which lets us know we messed up somewhere else in the code. + if len(self.get_marked_players()) == 0: + raise Exception("no marked players!") + + if self.elimination_timer_display > 1: + self.elimination_timer_display -= 1 # Decrease our timer by one second. + sound_volume = 1.0 / marked_player_amount + + for target in marked_players: + self._tick_sound.play( + sound_volume, + position=target.actor.node.position, + ) + target.actor.marked_timer_text.text = str(self.elimination_timer_display) + + # When counting down 3, 2, 1 play some dramatic sounds + if self.elimination_timer_display <= 3: + # We store our dramatic sounds in an array, so we target a specific element on the array + # depending on time remaining. Arrays start at index 0, so we need to decrease + # our variable by 1 to get the element index. + _tick_sound = self._danger_tick_sounds[self.elimination_timer_display - 1] + _tick_sound.play( + 1.5 + ) + else: + # Elimination timer is up! Let's eliminate all marked players. + self.elimination_timer_display -= 1 # Decrease our timer by one second. + self._eliminate_marked_players() + + # This function explodes all marked players + def _eliminate_marked_players(self) -> None: + self.marked_tick_timer = None + for target in self.get_marked_players(): + target.set_state(PlayerState.ELIMINATED) + target.actor.marked_timer_text.text = '' + + Blast(position=target.actor.node.position, + velocity=target.actor.node.velocity, + blast_radius=3.0, + source_player=target).autoretain() + ba.emitfx(position=target.actor.node.position, + velocity=target.actor.node.velocity, + count=int(16.0+random.random()*60), + scale=1.5, + spread=2, + chunk_type='spark') + target.actor.handlemessage(ba.DieMessage(how='marked_elimination')) + target.actor.shatter(extreme=True) + + self.match_placement.append(target.team) + + self._player_eliminated_sound.play( + 1.0 + ) + + # Let the gamemode know a Marked + self.marked_players_died() + + # This function should be called when a Marked player dies, like when timer runs out or they leave the game. + def marked_players_died(self) -> bool: + alive_players = self.get_alive_players() + # Is there only one player remaining? Or none at all? Let's end the gamemode + if len(alive_players) < 2: + if len(alive_players) == 1: + # Let's add our lone survivor to the match placement list. + self.match_placement.append(alive_players[0].team) + # Wait a while to let this sink in before we announce our victor. + self._end_game_timer = ba.Timer(1.25, ba.Call(self.end_game)) + else: + # There's still players remaining, so let's wait a while before marking a new player. + self.new_mark_timer = ba.Timer(2.0 if self.slow_motion else 4.0, ba.Call(self.new_mark)) + + # Another extensively used function that returns all alive players. + def get_alive_players(self) -> Sequence[ba.Player]: + alive_players = [] + for player in self.players: + if player.state == PlayerState.ELIMINATED: + continue # Ignore players who have been eliminated + if player.is_alive(): + alive_players.append(player) + return alive_players + + # This function is called every time we want to start a new "round" by marking a random player. + def new_mark(self) -> None: + # return + + # Don't mark a new player if we've already announced a victor. + if self.has_ended(): + return + + possible_targets = self.get_alive_players() + all_victims = [] + # Let's mark TWO players at once if there's 6 or more players. Helps with the pacing. + multi_choice = len(possible_targets) > 5 + + if multi_choice: + # Pick our first victim at random. + first_victim = random.choice(possible_targets) + all_victims.append(first_victim) + possible_targets.remove(first_victim) + # Let's pick our second victim, but this time excluding the player we picked earlier. + all_victims.append(random.choice(possible_targets)) + else: + # Pick one victim at random. + all_victims = [random.choice(possible_targets)] + + # Set time until marked players explode + self.elimination_timer_display = self.settings['Elimination Timer'] + # Set a timer that calls _eliminate_tick every second + self.marked_tick_timer = ba.Timer(1.0, ba.Call(self._eliminate_tick), repeat=True) + # Mark all chosen victims and play a sound + for new_victim in all_victims: + # _marked_sounds is an array. + # To make a nice marked sound effect, I play multiple sounds at once + # All of them are contained in the array. + for sound in self._marked_sounds: + sound.play( + 1.0, + position=new_victim.actor.node.position, + ) + self.mark(new_victim) + + # This function is called when the gamemode first loads. + @override + def on_begin(self) -> None: + super().on_begin() # Do standard gamemode on_begin behavior + + self.elimination_timer_display = 0 + self.match_placement = [] + + # End the game if there's only one player + if len(self.players) < 2: + self.match_placement.append(self.players[0].team) + self._round_end_timer = ba.Timer(0.5, self.end_game) + else: + # Pick random player(s) to get marked + self.new_mark_timer = ba.Timer(2.0 if self.slow_motion else 5.2, ba.Call(self.new_mark)) + + self._update_icons() # Create player state icons + + # This function creates and positions player state icons + def _update_icons(self): + count = len(self.teams) + x_offs = 100 + xval = x_offs * (count - 1) * -0.5 + # FUN FACT: In FFA games, every player belongs to a one-player team. + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + player.icon.set_position_and_scale((xval, 50), 0.8) + xval += x_offs + + # Hot Potato can be a bit much, so I opted to show gameplay tips at the start of the match. + # However because I put player state icons, the tips overlay the icons. + # I'm gonna modify this function to move the tip text above the icons. + def _show_tip(self) -> None: + + from bascenev1._gameutils import animate, GameTip + from bauiv1 import SpecialChar + from babase._language import Lstr + + # If there's any tips left on the list, display one. + if self.tips: + tip = self.tips.pop(random.randrange(len(self.tips))) + tip_title = Lstr(value='${A}:', + subs=[('${A}', Lstr(resource='tipText'))]) + icon: ba.Texture | None = None + sound: ba.Sound | None = None + if isinstance(tip, GameTip): + icon = tip.icon + sound = tip.sound + tip = tip.text + assert isinstance(tip, str) + + # Do a few replacements. + tip_lstr = Lstr(translate=('tips', tip), + subs=[('${PICKUP}', + babase.charstr(SpecialChar.TOP_BUTTON))]) + base_position = (75, 50) + tip_scale = 0.8 + tip_title_scale = 1.2 + vrmode = babase.app.env.vr # ba.app.vr_mode + + t_offs = -350.0 + height_offs = 100.0 + tnode = ba.newnode('text', + attrs={ + 'text': tip_lstr, + 'scale': tip_scale, + 'maxwidth': 900, + 'position': (base_position[0] + t_offs, + base_position[1] + height_offs), + 'h_align': 'left', + 'vr_depth': 300, + 'shadow': 1.0 if vrmode else 0.5, + 'flatness': 1.0 if vrmode else 0.5, + 'v_align': 'center', + 'v_attach': 'bottom' + }) + t2pos = (base_position[0] + t_offs - (20 if icon is None else 82), + base_position[1] + 2 + height_offs) + t2node = ba.newnode('text', + owner=tnode, + attrs={ + 'text': tip_title, + 'scale': tip_title_scale, + 'position': t2pos, + 'h_align': 'right', + 'vr_depth': 300, + 'shadow': 1.0 if vrmode else 0.5, + 'flatness': 1.0 if vrmode else 0.5, + 'maxwidth': 140, + 'v_align': 'center', + 'v_attach': 'bottom' + }) + if icon is not None: + ipos = (base_position[0] + t_offs - 40, base_position[1] + 1 + height_offs) + img = ba.newnode('image', + attrs={ + 'texture': icon, + 'position': ipos, + 'scale': (50, 50), + 'opacity': 1.0, + 'vr_depth': 315, + 'color': (1, 1, 1), + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) + ba.timer(5.0, img.delete) + if sound is not None: + sound.play() + + combine = ba.newnode('combine', + owner=tnode, + attrs={ + 'input0': 1.0, + 'input1': 0.8, + 'input2': 1.0, + 'size': 4 + }) + combine.connectattr('output', tnode, 'color') + combine.connectattr('output', t2node, 'color') + animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) + ba.timer(5.0, tnode.delete) + + # This function is called when a player leaves the game. + # This is only called when the player already joined with a character. + @override + def player_left(self, player: Player) -> None: + # If the leaving player is marked, remove the mark + if player.state == PlayerState.MARKED: + self.remove_mark(player) + + # If the leaving player is stunned, remove all stun timers + elif player.state == PlayerState.STUNNED: + player.stunned_timer = None + player.stunned_update_timer = None + + if len(self.get_marked_players()) == len(self.get_alive_players()): + for i in self.get_marked_players(): + self.remove_mark(i) + + if len(self.get_marked_players()) == 0: + self.marked_tick_timer = None + self.marked_players_died() + + player.set_state(PlayerState.ELIMINATED) + + def get_spawn_position(self, respawning_player: Player) -> ba.Vec3: + + # Get all spawn points + spawn_points = self.map.ffa_spawn_points + + # Calculate the average distance of each spawn point to all alive players + # Make a copy of the self.players array + players_to_count = self.players.copy() + if respawning_player and respawning_player in players_to_count: + players_to_count.remove(respawning_player) + + # Use default FFA spawning if there's no players or more than 4 + player_pts = [player.position for player in players_to_count if player.is_alive()] + if len(player_pts) <= 0 or len(player_pts) > 4: + return self.map.get_ffa_start_position(self.players) + + target_distance = 4.0 # This distance seems to be the sweet spot + best_point = None + best_distance_diff = float('inf') + + def _getpt() -> Sequence[float]: + point = self.map.ffa_spawn_points[self._next_ffa_start_index] + self._next_ffa_start_index = (self._next_ffa_start_index + 1) % len( + self.map.ffa_spawn_points + ) + x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3]) + z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5]) + point = ( + point[0] + random.uniform(*x_range), + point[1], + point[2] + random.uniform(*z_range), + ) + return point + + # Try a buncha times to find a good point that's not too far from every player + for _i in range(10): + for box in self.map.ffa_spawn_points: + point = _getpt() + avg_distance = sum( + (player_pt - ba.Vec3(point[0], point[1], point[2])).length() for player_pt in player_pts) / len(player_pts) + distance_diff = abs(avg_distance - target_distance) + if distance_diff < best_distance_diff: + best_distance_diff = distance_diff + best_point = point + + # Go through all players and return distance to the closest player to the best point + closest_distance = float('inf') + for i in player_pts: + distance = (i - ba.Vec3(best_point[0], best_point[1], best_point[2])).length() + if distance < closest_distance: + closest_distance = distance + + # If we're too close, we give up and just do a regular FFA spawn + if closest_distance < 2.0: + return self.map.get_ffa_start_position(self.players) + + return best_point + + # This function is called every time a player spawns + @override + def spawn_player(self, player: Player) -> ba.Actor: + position = self.get_spawn_position(player) + position = (position[0], + position[1] - 0.3, # Move the spawn a bit lower + position[2]) + + name = player.getname() + + light_color = ba.normalized_color(player.color) + display_color = ba.safecolor(player.color, target_intensity=0.75) + + # Here we actually crate the player character + spaz = PotatoPlayerSpaz(color=player.color, + highlight=player.highlight, + character=player.character, + player=player) + + player.actor = spaz # Assign player character to the owner + + spaz.node.name = name + spaz.node.name_color = display_color + + if player.state != PlayerState.STUNNED: + spaz.connect_controls_to_player() + + # Move to the stand position and add a flash of light + spaz.handlemessage(ba.StandMessage(position, random.uniform(0, 360))) + t = ba.time() + self._spawn_sound.play( + 1.0, + position=spaz.node.position, + ) + light = ba.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + ba.animate(light, 'intensity', {0: 0, + 0.25: 1, + 0.5: 0}) + ba.timer(0.5, light.delete) + + # Game reacts to various events + @override + def handlemessage(self, msg: Any) -> Any: + # This is called if the player dies. + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) + player = msg.getplayer(Player) + + # If a player gets eliminated, don't respawn + if msg.how == 'marked_elimination': + return + + self.spawn_player(player) # Spawn a new player character + + # If a REGULAR player dies, they respawn STUNNED. + # If a STUNNED player dies, reapply all visual effects. + if player.state in [PlayerState.REGULAR, PlayerState.STUNNED]: + player.set_state(PlayerState.STUNNED) + + # If a MARKED player falls off the map, apply the MARKED effects on the new spaz that respawns. + if player.state == PlayerState.MARKED: + self.mark(player) + + # This is called when we want to end the game and announce the winner + @override + def end_game(self) -> None: + # Proceed only if the game hasn't ended yet. + if self.has_ended(): + return + results = ba.GameResults() + # By this point our match placement list should be filled with all players. + # Players that died/left earliest should be the first entries. + # We're gonna use array indexes to decide match placements. + # Because of that, we're gonna flip the order of our array, so the last entries are first. + self.match_placement.reverse() + for team in self.teams: + # Use each player's index in the array for our scoring + # 0 is the first index, so we add 1 to the score. + results.set_team_score(team, self.match_placement.index(team) + 1) + self.end(results=results) # Standard game ending behavior diff --git a/plugins/minigames/hyper_race.py b/plugins/minigames/hyper_race.py new file mode 100644 index 000000000..c5f5aaa91 --- /dev/null +++ b/plugins/minigames/hyper_race.py @@ -0,0 +1,1238 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +from dataclasses import dataclass + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1 import _map +from bascenev1lib.actor.bomb import Bomb, Blast, BombFactory +from bascenev1lib.actor.powerupbox import PowerupBox +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict, + Union) + from bascenev1lib.actor.onscreentimer import OnScreenTimer + + +class ThePadDefs: + points = {} + boxes = {} + points['race_mine1'] = (0, 5, 12) + points['race_point1'] = (0.2, 5, 2.86308) + (0.507, 4.673, 1.1) + points['race_point2'] = (6.9301, 5.04988, 2.82066) + (0.911, 4.577, 1.073) + points['race_point3'] = (6.98857, 4.5011, -8.88703) + (1.083, 4.673, 1.076) + points['race_point4'] = (-6.4441, 4.5011, -8.88703) + (1.083, 4.673, 1.076) + points['race_point5'] = (-6.31128, 4.5011, 2.82669) + (0.894, 4.673, 0.941) + boxes['area_of_interest_bounds'] = ( + 0.3544110667, 4.493562578, -2.518391331) + ( + 0.0, 0.0, 0.0) + (16.64754831, 8.06138989, 18.5029888) + points['ffa_spawn1'] = (-0, 5, 2.5) + points['flag1'] = (-7.026110145, 4.308759233, -6.302807727) + points['flag2'] = (7.632557137, 4.366002373, -6.287969342) + points['flagDefault'] = (0.4611826686, 4.382076338, 3.680881802) + boxes['map_bounds'] = (0.2608783669, 4.899663734, -3.543675157) + ( + 0.0, 0.0, 0.0) + (29.23565494, 14.19991443, 29.92689344) + points['powerup_spawn1'] = (-4.166594349, 5.281834349, -6.427493781) + points['powerup_spawn2'] = (4.426873526, 5.342460464, -6.329745237) + points['powerup_spawn3'] = (-4.201686731, 5.123385835, 0.4400721376) + points['powerup_spawn4'] = (4.758924722, 5.123385835, 0.3494054559) + points['shadow_lower_bottom'] = (-0.2912522507, 2.020798381, 5.341226521) + points['shadow_lower_top'] = (-0.2912522507, 3.206066063, 5.341226521) + points['shadow_upper_bottom'] = (-0.2912522507, 6.062361813, 5.341226521) + points['shadow_upper_top'] = (-0.2912522507, 9.827201965, 5.341226521) + points['spawn1'] = (-0, 5, 2.5) + points['tnt1'] = (0.4599593402, 4.044276501, -6.573537395) + + +class ThePadMapb(bs.Map): + defs = ThePadDefs() + name = 'Racing' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['hyper'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'thePadPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'mesh': bs.getmesh('thePadLevel'), + 'bottom_mesh': bs.getmesh('thePadLevelBottom'), + 'collision_mesh': bs.getcollisionmesh('thePadLevelCollide'), + 'tex': bs.gettexture('thePadLevelColor'), + 'bgtex': bs.gettexture('black'), + 'bgmesh': bs.getmesh('thePadBG'), + 'railing_collision_mesh': bs.getcollisionmesh('thePadLevelBumper'), + 'vr_fill_mound_mesh': bs.getmesh('thePadVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound') + } + # fixme should chop this into vr/non-vr sections for efficiency + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.bottom = bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['bottom_mesh'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + self.railing = bs.newnode( + 'terrain', + attrs={ + 'collision_mesh': self.preloaddata['railing_collision_mesh'], + 'materials': [shared.railing_material], + 'bumper': True + }) + bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['vr_fill_mound_mesh'], + 'lighting': False, + 'vr_only': True, + 'color': (0.56, 0.55, 0.47), + 'background': True, + 'color_texture': self.preloaddata['vr_fill_mound_tex'] + }) + gnode = bs.getactivity().globalsnode + gnode.tint = (1.1, 1.1, 1.0) + gnode.ambient_color = (1.1, 1.1, 1.0) + gnode.vignette_outer = (0.7, 0.65, 0.75) + gnode.vignette_inner = (0.95, 0.95, 0.93) + + +# ba_meta export plugin +class NewMap(babase.Plugin): + """My first ballistica plugin!""" + + def on_app_running(self) -> None: + _map.register_map(ThePadMapb) + + +class NewBlast(Blast): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + blast_radius: float = 2.0, + blast_type: str = 'normal', + source_player: bs.Player = None, + hit_type: str = 'explosion', + hit_subtype: str = 'normal'): + bs.Actor.__init__(self) + + shared = SharedObjects.get() + factory = BombFactory.get() + + self.blast_type = blast_type + self._source_player = source_player + self.hit_type = hit_type + self.hit_subtype = hit_subtype + self.radius = blast_radius + + # Set our position a bit lower so we throw more things upward. + rmats = (factory.blast_material, shared.attack_material) + self.node = bs.newnode( + 'region', + delegate=self, + attrs={ + 'position': (position[0], position[1] - 0.1, position[2]), + 'scale': (self.radius, self.radius, self.radius), + 'type': 'sphere', + 'materials': rmats + }, + ) + + bs.timer(0.05, self.node.delete) + + # Throw in an explosion and flash. + evel = (velocity[0], max(-1.0, velocity[1]), velocity[2]) + explosion = bs.newnode('explosion', + attrs={ + 'position': position, + 'velocity': evel, + 'radius': self.radius, + 'big': (self.blast_type == 'tnt') + }) + if self.blast_type == 'ice': + explosion.color = (0, 0.05, 0.4) + + bs.timer(1.0, explosion.delete) + + if self.blast_type != 'ice': + bs.emitfx(position=position, + velocity=velocity, + count=int(1.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='thin_smoke') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='ice' if self.blast_type == 'ice' else 'smoke') + bs.emitfx(position=position, + emit_type='distortion', + spread=1.0 if self.blast_type == 'tnt' else 2.0) + + # And emit some shrapnel. + if self.blast_type == 'ice': + + def emit() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=30, + spread=2.0, + scale=0.4, + chunk_type='ice', + emit_type='stickers') + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + elif self.blast_type == 'sticky': + + def emit() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + spread=0.7, + chunk_type='slime') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.5, + spread=0.7, + chunk_type='slime') + bs.emitfx(position=position, + velocity=velocity, + count=15, + scale=0.6, + chunk_type='slime', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=20, + scale=0.7, + chunk_type='spark', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=int(6.0 + random.random() * 12), + scale=0.8, + spread=1.5, + chunk_type='spark') + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + elif self.blast_type == 'impact': + + def emit() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.8, + chunk_type='metal') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.4, + chunk_type='metal') + bs.emitfx(position=position, + velocity=velocity, + count=20, + scale=0.7, + chunk_type='spark', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=int(8.0 + random.random() * 15), + scale=0.8, + spread=1.5, + chunk_type='spark') + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + else: # Regular or land mine bomb shrapnel. + + def emit() -> None: + if self.blast_type != 'tnt': + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + chunk_type='rock') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.5, + chunk_type='rock') + bs.emitfx(position=position, + velocity=velocity, + count=30, + scale=1.0 if self.blast_type == 'tnt' else 0.7, + chunk_type='spark', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=int(18.0 + random.random() * 20), + scale=1.0 if self.blast_type == 'tnt' else 0.8, + spread=1.5, + chunk_type='spark') + + # TNT throws splintery chunks. + if self.blast_type == 'tnt': + + def emit_splinters() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(20.0 + random.random() * 25), + scale=0.8, + spread=1.0, + chunk_type='splinter') + + bs.timer(0.01, emit_splinters) + + # Every now and then do a sparky one. + if self.blast_type == 'tnt' or random.random() < 0.1: + + def emit_extra_sparks() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(10.0 + random.random() * 20), + scale=0.8, + spread=1.5, + chunk_type='spark') + + bs.timer(0.02, emit_extra_sparks) + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + lcolor = ((0.6, 0.6, 1.0) if self.blast_type == 'ice' else + (1, 0.3, 0.1)) + light = bs.newnode('light', + attrs={ + 'position': position, + 'volume_intensity_scale': 10.0, + 'color': lcolor + }) + + scl = random.uniform(0.6, 0.9) + scorch_radius = light_radius = self.radius + if self.blast_type == 'tnt': + light_radius *= 1.4 + scorch_radius *= 1.15 + scl *= 3.0 + + iscale = 1.6 + bs.animate( + light, 'intensity', { + 0: 2.0 * iscale, + scl * 0.02: 0.1 * iscale, + scl * 0.025: 0.2 * iscale, + scl * 0.05: 17.0 * iscale, + scl * 0.06: 5.0 * iscale, + scl * 0.08: 4.0 * iscale, + scl * 0.2: 0.6 * iscale, + scl * 2.0: 0.00 * iscale, + scl * 3.0: 0.0 + }) + bs.animate( + light, 'radius', { + 0: light_radius * 0.2, + scl * 0.05: light_radius * 0.55, + scl * 0.1: light_radius * 0.3, + scl * 0.3: light_radius * 0.15, + scl * 1.0: light_radius * 0.05 + }) + bs.timer(scl * 3.0, light.delete) + + # Make a scorch that fades over time. + scorch = bs.newnode('scorch', + attrs={ + 'position': position, + 'size': scorch_radius * 0.5, + 'big': (self.blast_type == 'tnt') + }) + if self.blast_type == 'ice': + scorch.color = (1, 1, 1.5) + + bs.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) + bs.timer(13.0, scorch.delete) + + if self.blast_type == 'ice': + factory.hiss_sound.play(position=light.position) + + lpos = light.position + factory.random_explode_sound().play(position=lpos) + factory.debris_fall_sound.play(position=lpos) + + bs.camerashake(0.0) + + # TNT is more epic. + if self.blast_type == 'tnt': + factory.random_explode_sound().play(position=lpos) + + def _extra_boom() -> None: + factory.random_explode_sound().play(position=lpos) + + bs.timer(0.25, _extra_boom) + + def _extra_debris_sound() -> None: + factory.debris_fall_sound.play(position=lpos) + factory.wood_debris_fall_sound.play(position=lpos) + + bs.timer(0.4, _extra_debris_sound) + + +class NewBomb(Bomb): + + def explode(self) -> None: + """Blows up the bomb if it has not yet done so.""" + if self._exploded: + return + self._exploded = True + if self.node: + blast = NewBlast(position=self.node.position, + velocity=self.node.velocity, + blast_radius=self.blast_radius, + blast_type=self.bomb_type, + source_player=babase.existing(self._source_player), + hit_type=self.hit_type, + hit_subtype=self.hit_subtype).autoretain() + for callback in self._explode_callbacks: + callback(self, blast) + + # We blew up so we need to go away. + # NOTE TO SELF: do we actually need this delay? + bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage())) + + +class TNT(bs.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + tnt_scale: float = 1.0, + teleport: bool = True): + super().__init__() + self.position = position + self.teleport = teleport + + self._no_collide_material = bs.Material() + self._no_collide_material.add_actions( + actions=('modify_part_collision', 'collide', False), + ) + self._collide_material = bs.Material() + self._collide_material.add_actions( + actions=('modify_part_collision', 'collide', True), + ) + + if teleport: + collide = self._collide_material + else: + collide = self._no_collide_material + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'mesh': bs.getmesh('tnt'), + 'color_texture': bs.gettexture('tnt'), + 'body': 'crate', + 'mesh_scale': tnt_scale, + 'body_scale': tnt_scale, + 'density': 2.0, + 'gravity_scale': 2.0, + 'materials': [collide] + } + ) + if not teleport: + bs.timer(0.1, self._collide) + + def _collide(self) -> None: + self.node.materials += (self._collide_material,) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.OutOfBoundsMessage): + if self.teleport: + self.node.position = self.position + self.node.velocity = (0, 0, 0) + else: + self.node.delete() + else: + super().handlemessage(msg) + + +class RaceRegion(bs.Actor): + """Region used to track progress during a race.""" + + def __init__(self, pt: Sequence[float], index: int): + super().__init__() + activity = self.activity + assert isinstance(activity, RaceGame) + self.pos = pt + self.index = index + self.node = bs.newnode( + 'region', + delegate=self, + attrs={ + 'position': pt[:3], + 'scale': (pt[3] * 2.0, pt[4] * 2.0, pt[5] * 2.0), + 'type': 'box', + 'materials': [activity.race_region_material] + }) + + +# MINIGAME +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.distance_txt: Optional[bs.Node] = None + self.last_region = 0 + self.lap = 0 + self.distance = 0.0 + self.finished = False + self.rank: Optional[int] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.time: Optional[float] = None + self.lap = 0 + self.finished = False + + +# ba_meta export bascenev1.GameActivity +class RaceGame(bs.TeamGameActivity[Player, Team]): + """Game of racing around a track.""" + + name = 'Hyper Race' + description = 'Creado Por Cebolla!!' + scoreconfig = bs.ScoreConfig(label='Time', + lower_is_better=True, + scoretype=bs.ScoreType.MILLISECONDS) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting('Laps', min_value=1, default=3, increment=1), + bs.IntChoiceSetting( + 'Time Limit', + default=0, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + ), + bs.BoolSetting('Epic Mode', default=False), + ] + + # We have some specific settings in teams mode. + if issubclass(sessiontype, bs.DualTeamSession): + settings.append( + bs.BoolSetting('Entire Team Must Finish', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.MultiTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('hyper') + + def __init__(self, settings: dict): + self._race_started = False + super().__init__(settings) + self.factory = factory = BombFactory.get() + self.shared = shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._score_sound = bs.getsound('score') + self._swipsound = bs.getsound('swip') + self._last_team_time: Optional[float] = None + self._front_race_region: Optional[int] = None + self._nub_tex = bs.gettexture('nub') + self._beep_1_sound = bs.getsound('raceBeep1') + self._beep_2_sound = bs.getsound('raceBeep2') + self.race_region_material: Optional[bs.Material] = None + self._regions: List[RaceRegion] = [] + self._team_finish_pts: Optional[int] = None + self._time_text: Optional[bs.Actor] = None + self._timer: Optional[OnScreenTimer] = None + self._scoreboard_timer: Optional[bs.Timer] = None + self._player_order_update_timer: Optional[bs.Timer] = None + self._start_lights: Optional[List[bs.Node]] = None + self._laps = int(settings['Laps']) + self._entire_team_must_finish = bool( + settings.get('Entire Team Must Finish', False)) + self._time_limit = float(settings['Time Limit']) + self._epic_mode = bool(settings['Epic Mode']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC_RACE + if self._epic_mode else bs.MusicType.RACE) + + self._safe_region_material = bs.Material() + self._safe_region_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True)) + ) + + def get_instance_description(self) -> Union[str, Sequence]: + if (isinstance(self.session, bs.DualTeamSession) + and self._entire_team_must_finish): + t_str = ' Your entire team has to finish.' + else: + t_str = '' + + if self._laps > 1: + return 'Run ${ARG1} laps.' + t_str, self._laps + return 'Run 1 lap.' + t_str + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._laps > 1: + return 'run ${ARG1} laps', self._laps + return 'run 1 lap' + + def on_transition_in(self) -> None: + super().on_transition_in() + shared = SharedObjects.get() + pts = self.map.get_def_points('race_point') + mat = self.race_region_material = bs.Material() + mat.add_actions(conditions=('they_have_material', + shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + self._handle_race_point_collide), + )) + for rpt in pts: + self._regions.append(RaceRegion(rpt, len(self._regions))) + + bs.newnode( + 'region', + attrs={ + 'position': (0.3, 4.044276501, -2.9), + 'scale': (11.7, 15, 9.5), + 'type': 'box', + 'materials': [self._safe_region_material] + } + ) + + def _flash_player(self, player: Player, scale: float) -> None: + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + pos = player.actor.node.position + light = bs.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + bs.timer(0.5, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) + + def _handle_race_point_collide(self) -> None: + # FIXME: Tidy this up. + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + collision = bs.getcollision() + try: + region = collision.sourcenode.getdelegate(RaceRegion, True) + spaz = collision.opposingnode.getdelegate(PlayerSpaz, True) + except bs.NotFoundError: + return + + if not spaz.is_alive(): + return + + try: + player = spaz.getplayer(Player, True) + except bs.NotFoundError: + return + + last_region = player.last_region + this_region = region.index + + if last_region != this_region: + + # If a player tries to skip regions, smite them. + # Allow a one region leeway though (its plausible players can get + # blown over a region, etc). + if this_region > last_region + 2: + if player.is_alive(): + assert player.actor + player.actor.handlemessage(bs.DieMessage()) + bs.broadcastmessage(babase.Lstr( + translate=('statements', 'Killing ${NAME} for' + ' skipping part of the track!'), + subs=[('${NAME}', player.getname(full=True))]), + color=(1, 0, 0)) + else: + # If this player is in first, note that this is the + # front-most race-point. + if player.rank == 0: + self._front_race_region = this_region + + player.last_region = this_region + if last_region >= len(self._regions) - 2 and this_region == 0: + team = player.team + player.lap = min(self._laps, player.lap + 1) + + # In teams mode with all-must-finish on, the team lap + # value is the min of all team players. + # Otherwise its the max. + if isinstance(self.session, bs.DualTeamSession + ) and self._entire_team_must_finish: + team.lap = min([p.lap for p in team.players]) + else: + team.lap = max([p.lap for p in team.players]) + + # A player is finishing. + if player.lap == self._laps: + + # In teams mode, hand out points based on the order + # players come in. + if isinstance(self.session, bs.DualTeamSession): + assert self._team_finish_pts is not None + if self._team_finish_pts > 0: + self.stats.player_scored(player, + self._team_finish_pts, + screenmessage=False) + self._team_finish_pts -= 25 + + # Flash where the player is. + self._flash_player(player, 1.0) + player.finished = True + assert player.actor + player.actor.handlemessage( + bs.DieMessage(immediate=True)) + + # Makes sure noone behind them passes them in rank + # while finishing. + player.distance = 9999.0 + + # If the whole team has finished the race. + if team.lap == self._laps: + self._score_sound.play() + player.team.finished = True + assert self._timer is not None + elapsed = bs.time() - self._timer.getstarttime() + self._last_team_time = player.team.time = elapsed + self._check_end_game() + + # Team has yet to finish. + else: + self._swipsound.play() + + # They've just finished a lap but not the race. + else: + self._swipsound.play() + self._flash_player(player, 0.3) + + # Print their lap number over their head. + try: + assert isinstance(player.actor, PlayerSpaz) + mathnode = bs.newnode('math', + owner=player.actor.node, + attrs={ + 'input1': (0, 1.9, 0), + 'operation': 'add' + }) + player.actor.node.connectattr( + 'torso_position', mathnode, 'input2') + tstr = babase.Lstr(resource='lapNumberText', + subs=[('${CURRENT}', + str(player.lap + 1)), + ('${TOTAL}', str(self._laps)) + ]) + txtnode = bs.newnode('text', + owner=mathnode, + attrs={ + 'text': tstr, + 'in_world': True, + 'color': (1, 1, 0, 1), + 'scale': 0.015, + 'h_align': 'center' + }) + mathnode.connectattr('output', txtnode, 'position') + bs.animate(txtnode, 'scale', { + 0.0: 0, + 0.2: 0.019, + 2.0: 0.019, + 2.2: 0 + }) + bs.timer(2.3, mathnode.delete) + except Exception: + babase.print_exception('Error printing lap.') + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + + # A player leaving disqualifies the team if 'Entire Team Must Finish' + # is on (otherwise in teams mode everyone could just leave except the + # leading player to win). + if (isinstance(self.session, bs.DualTeamSession) + and self._entire_team_must_finish): + bs.broadcastmessage(babase.Lstr( + translate=('statements', + '${TEAM} is disqualified because ${PLAYER} left'), + subs=[('${TEAM}', player.team.name), + ('${PLAYER}', player.getname(full=True))]), + color=(1, 1, 0)) + player.team.finished = True + player.team.time = None + player.team.lap = 0 + bs.getsound('boo').play() + for otherplayer in player.team.players: + otherplayer.lap = 0 + otherplayer.finished = True + try: + if otherplayer.actor is not None: + otherplayer.actor.handlemessage(bs.DieMessage()) + except Exception: + babase.print_exception('Error sending DieMessage.') + + # Defer so team/player lists will be updated. + babase.pushcall(self._check_end_game) + + def _update_scoreboard(self) -> None: + for team in self.teams: + distances = [player.distance for player in team.players] + if not distances: + teams_dist = 0.0 + else: + if (isinstance(self.session, bs.DualTeamSession) + and self._entire_team_must_finish): + teams_dist = min(distances) + else: + teams_dist = max(distances) + self._scoreboard.set_team_value( + team, + teams_dist, + self._laps, + flash=(teams_dist >= float(self._laps)), + show_value=False) + + def on_begin(self) -> None: + from bascenev1lib.actor.onscreentimer import OnScreenTimer + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + self._team_finish_pts = 100 + + # Throw a timer up on-screen. + self._time_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.5, 1), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, -50), + 'scale': 1.4, + 'text': '' + })) + self._timer = OnScreenTimer() + + self._scoreboard_timer = bs.Timer(0.25, + self._update_scoreboard, + repeat=True) + self._player_order_update_timer = bs.Timer(0.25, + self._update_player_order, + repeat=True) + + if self.slow_motion: + t_scale = 0.4 + light_y = 50 + else: + t_scale = 1.0 + light_y = 150 + lstart = 7.1 * t_scale + inc = 1.25 * t_scale + + bs.timer(lstart, self._do_light_1) + bs.timer(lstart + inc, self._do_light_2) + bs.timer(lstart + 2 * inc, self._do_light_3) + bs.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = bs.newnode('image', + attrs={ + 'texture': bs.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + bs.animate( + lnub, 'opacity', { + 4.0 * t_scale: 0, + 5.0 * t_scale: 1.0, + 12.0 * t_scale: 1.0, + 12.5 * t_scale: 0.0 + }) + bs.timer(13.0 * t_scale, lnub.delete) + self._start_lights.append(lnub) + + self._obstacles() + + pts = self.map.get_def_points('race_point') + for rpt in pts: + bs.newnode( + 'locator', + attrs={ + 'shape': 'circle', + 'position': (rpt[0], 4.382076338, rpt[2]), + 'size': (rpt[3] * 2.0, 0, rpt[5] * 2.0), + 'color': (0, 1, 0), + 'opacity': 1.0, + 'draw_beauty': False, + 'additive': True + } + ) + + def _obstacles(self) -> None: + self._start_lights[0].color = (0.2, 0, 0) + self._start_lights[1].color = (0.2, 0, 0) + self._start_lights[2].color = (0.2, 0.05, 0) + self._start_lights[3].color = (0.0, 0.3, 0) + + self._tnt((1.5, 5, 2.3), (0, 0, 0), 1.0) + self._tnt((1.5, 5, 3.3), (0, 0, 0), 1.0) + + self._tnt((3.5, 5, 2.3), (0, 0, 0), 1.0) + self._tnt((3.5, 5, 3.3), (0, 0, 0), 1.0) + + self._tnt((5.5, 5, 2.3), (0, 0, 0), 1.0) + self._tnt((5.5, 5, 3.3), (0, 0, 0), 1.0) + + self._tnt((-6, 5, -7), (0, 0, 0), 1.3) + self._tnt((-7, 5, -5), (0, 0, 0), 1.3) + self._tnt((-6, 5, -3), (0, 0, 0), 1.3) + self._tnt((-7, 5, -1), (0, 0, 0), 1.3) + self._tnt((-6, 5, 1), (0, 0, 0), 1.3) + + bs.timer(0.1, bs.WeakCall(self._tnt, (-3.2, 5, 1), + (0, 0, 0), 1.0, (0, 20, 60)), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (-5, 5, 0), (0, 0, 0), 1.0, 1.0, (0, 20, 3)), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (-1.5, 5, 0), (0, 0, 0), 1.0, 1.0, (0, 20, 3)), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-1, 5, -8), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-1, 5, -9), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-1, 5, -10), (0, 10, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-4.6, 5, -8), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-4.6, 5, -9), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-4.6, 5, -10), (0, 10, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall( + self._powerup, (2, 5, -5), 'curse', (0, 20, -3)), repeat=True) + bs.timer(1.6, bs.WeakCall( + self._powerup, (4, 5, -5), 'curse', (0, 20, -3)), repeat=True) + + def _tnt(self, + position: float, + velocity: float, + tnt_scale: float, + extra_acceleration: float = None) -> None: + if extra_acceleration: + TNT(position, velocity, tnt_scale, False).autoretain( + ).node.extra_acceleration = extra_acceleration + else: + TNT(position, velocity, tnt_scale).autoretain() + + def _bomb(self, + type: str, + position: float, + velocity: float, + mesh_scale: float, + body_scale: float, + extra_acceleration: float = None) -> None: + if extra_acceleration: + NewBomb(position=position, + velocity=velocity, + bomb_type=type).autoretain( + ).node.extra_acceleration = extra_acceleration + else: + NewBomb(position=position, + velocity=velocity, + bomb_type=type).autoretain() + + def _powerup(self, + position: float, + poweruptype: str, + extra_acceleration: float = None) -> None: + if extra_acceleration: + PowerupBox(position=position, + poweruptype=poweruptype).autoretain( + ).node.extra_acceleration = extra_acceleration + else: + PowerupBox(position=position, poweruptype=poweruptype).autoretain() + + def _do_light_1(self) -> None: + assert self._start_lights is not None + self._start_lights[0].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + self._beep_1_sound.play() + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + self._beep_2_sound.play() + for player in self.players: + if player.actor is not None: + try: + assert isinstance(player.actor, PlayerSpaz) + player.actor.connect_controls_to_player() + except Exception: + babase.print_exception('Error in race player connects.') + assert self._timer is not None + self._timer.start() + + self._race_started = True + + def _update_player_order(self) -> None: + + # Calc all player distances. + for player in self.players: + pos: Optional[babase.Vec3] + try: + pos = player.position + except bs.NotFoundError: + pos = None + if pos is not None: + r_index = player.last_region + rg1 = self._regions[r_index] + r1pt = babase.Vec3(rg1.pos[:3]) + rg2 = self._regions[0] if r_index == len( + self._regions) - 1 else self._regions[r_index + 1] + r2pt = babase.Vec3(rg2.pos[:3]) + r2dist = (pos - r2pt).length() + amt = 1.0 - (r2dist / (r2pt - r1pt).length()) + amt = player.lap + (r_index + amt) * (1.0 / len(self._regions)) + player.distance = amt + + # Sort players by distance and update their ranks. + p_list = [(player.distance, player) for player in self.players] + + p_list.sort(reverse=True, key=lambda x: x[0]) + for i, plr in enumerate(p_list): + plr[1].rank = i + if plr[1].actor: + node = plr[1].distance_txt + if node: + node.text = str(i + 1) if plr[1].is_alive() else '' + + def spawn_player(self, player: Player) -> bs.Actor: + if player.team.finished: + # FIXME: This is not type-safe! + # This call is expected to always return an Actor! + # Perhaps we need something like can_spawn_player()... + # noinspection PyTypeChecker + return None # type: ignore + pos = self._regions[player.last_region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1], + pos[2] + random.uniform(*z_range)) + spaz = self.spawn_player_spaz( + player, position=pos, angle=90 if not self._race_started else None) + assert spaz.node + + # Prevent controlling of characters before the start of the race. + if not self._race_started: + spaz.disconnect_controls_from_player() + + mathnode = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + spaz.node.connectattr('torso_position', mathnode, 'input2') + + distance_txt = bs.newnode('text', + owner=spaz.node, + attrs={ + 'text': '', + 'in_world': True, + 'color': (1, 1, 0.4), + 'scale': 0.02, + 'h_align': 'center' + }) + player.distance_txt = distance_txt + mathnode.connectattr('output', distance_txt, 'position') + return spaz + + def _check_end_game(self) -> None: + + # If there's no teams left racing, finish. + teams_still_in = len([t for t in self.teams if not t.finished]) + if teams_still_in == 0: + self.end_game() + return + + # Count the number of teams that have completed the race. + teams_completed = len( + [t for t in self.teams if t.finished and t.time is not None]) + + if teams_completed > 0: + session = self.session + + # In teams mode its over as soon as any team finishes the race + + # FIXME: The get_ffa_point_awards code looks dangerous. + if isinstance(session, bs.DualTeamSession): + self.end_game() + else: + # In ffa we keep the race going while there's still any points + # to be handed out. Find out how many points we have to award + # and how many teams have finished, and once that matches + # we're done. + assert isinstance(session, bs.FreeForAllSession) + points_to_award = len(session.get_ffa_point_awards()) + if teams_completed >= points_to_award - teams_completed: + self.end_game() + return + + def end_game(self) -> None: + + # Stop updating our time text, and set it to show the exact last + # finish time if we have one. (so users don't get upset if their + # final time differs from what they see onscreen by a tiny amount) + assert self._timer is not None + if self._timer.has_started(): + self._timer.stop( + endtime=None if self._last_team_time is None else ( + self._timer.getstarttime() + self._last_team_time)) + + results = bs.GameResults() + + for team in self.teams: + if team.time is not None: + # We store time in seconds, but pass a score in milliseconds. + results.set_team_score(team, int(team.time * 1000.0)) + else: + results.set_team_score(team, None) + + # We don't announce a winner in ffa mode since its probably been a + # while since the first place guy crossed the finish line so it seems + # odd to be announcing that now. + self.end(results=results, + announce_winning_team=isinstance(self.session, + bs.DualTeamSession)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # Augment default behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + if not player.finished: + self.respawn_player(player, respawn_time=1) + else: + super().handlemessage(msg) diff --git a/plugins/minigames/icy_emits.py b/plugins/minigames/icy_emits.py new file mode 100644 index 000000000..06c904d2a --- /dev/null +++ b/plugins/minigames/icy_emits.py @@ -0,0 +1,48 @@ +# Made by your friend: Freaku + + +import babase +import bascenev1 as bs +import random +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.game.meteorshower import Player, MeteorShowerGame + + +# ba_meta require api 9 +# ba_meta export bascenev1.GameActivity +class IcyEmitsGame(MeteorShowerGame): + name = 'Icy Emits' + + @classmethod + def get_supported_maps(cls, sessiontype): + return ['Lake Frigid', 'Hockey Stadium'] + + def _drop_bomb_cluster(self) -> None: + delay = 0.0 + for _i in range(random.randrange(1, 3)): + # Drop them somewhere within our bounds with velocity pointing + # toward the opposite side. + pos = (-7.3 + 15.3 * random.random(), 5.3, + -5.5 + 2.1 * random.random()) + dropdir = (-1.0 if pos[0] > 0 else 1.0) + vel = (0, 10, 0) + bs.timer(delay, babase.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + def _drop_bomb(self, position, velocity): + random_xpositions = [-10, -9, -8, -7, -6, -5, - + 4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + random_zpositions = [-5, -4.5, -4, -3.5, -3, -2.5, -2, - + 1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5] + bomb_position = (random.choice(random_xpositions), 0.2, random.choice(random_zpositions)) + Bomb(position=bomb_position, velocity=velocity, bomb_type='ice').autoretain() + + +# ba_meta export babase.Plugin +class byFreaku(babase.Plugin): + def __init__(self): + ## Campaign support ## + randomPic = ['lakeFrigidPreview', 'hockeyStadiumPreview'] + babase.app.classic.add_coop_practice_level(bs.Level( + name='Icy Emits', displayname='${GAME}', gametype=IcyEmitsGame, settings={}, preview_texture_name=random.choice(randomPic))) diff --git a/plugins/minigames/infection.py b/plugins/minigames/infection.py new file mode 100644 index 000000000..b46b1e230 --- /dev/null +++ b/plugins/minigames/infection.py @@ -0,0 +1,526 @@ +"""New Duel / Created by: byANG3L""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import random +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + pass + +lang = bs.app.lang.language +if lang == 'Spanish': + name = 'Infección' + description = '¡Se está extendiendo!' + instance_description = '¡Evite la propagación!' + mines = 'Minas' + enable_bombs = 'Habilitar Bombas' + extra_mines = 'Seg/Mina Extra' + max_infected_size = 'Tamaño Máx. de Infección' + max_size_increases = 'Incrementar Tamaño Cada' + infection_spread_rate = 'Velocidad de Infección' + faster = 'Muy Rápido' + fast = 'Rápido' + normal = 'Normal' + slow = 'Lento' + slowest = 'Muy Lento' + insane = 'Insano' +else: + name = 'Infection' + description = "It's spreading!" + instance_description = 'Avoid the spread!' + mines = 'Mines' + enable_bombs = 'Enable Bombs' + extra_mines = 'Sec/Extra Mine' + max_infected_size = 'Max Infected Size' + max_size_increases = 'Max Size Increases Every' + infection_spread_rate = 'Infection Spread Rate' + faster = 'Faster' + fast = 'Fast' + normal = 'Normal' + slow = 'Slow' + slowest = 'Slowest' + insane = 'Insane' + + +def ba_get_api_version(): + return 8 + + +def ba_get_levels(): + return [bs._level.Level( + name, + gametype=Infection, + settings={}, + preview_texture_name='footballStadiumPreview')] + + +class myMine(Bomb): + # reason for the mine class is so we can add the death zone + def __init__(self, + pos: Sequence[float] = (0.0, 1.0, 0.0)): + Bomb.__init__(self, position=pos, bomb_type='land_mine') + showInSpace = False + self.died = False + self.rad = 0.3 + self.zone = bs.newnode( + 'locator', + attrs={ + 'shape': 'circle', + 'position': self.node.position, + 'color': (1, 0, 0), + 'opacity': 0.5, + 'draw_beauty': showInSpace, + 'additive': True}) + bs.animate_array( + self.zone, + 'size', + 1, + {0: [0.0], 0.05: [2*self.rad]}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if not self.died: + self.getactivity().mine_count -= 1 + self.died = True + bs.animate_array( + self.zone, + 'size', + 1, + {0: [2*self.rad], 0.05: [0]}) + self.zone = None + super().handlemessage(msg) + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: Optional[int] = None + self.death_time: Optional[float] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class Infection(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = name + description = description + + # Print messages when players die since it matters here. + announce_player_deaths = True + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + mines, + min_value=5, + default=10, + increment=5, + ), + bs.BoolSetting(enable_bombs, default=True), + bs.IntSetting( + extra_mines, + min_value=1, + default=10, + increment=1, + ), + bs.IntSetting( + max_infected_size, + min_value=4, + default=6, + increment=1, + ), + bs.IntChoiceSetting( + max_size_increases, + choices=[ + ('10s', 10), + ('20s', 20), + ('30s', 30), + ('1 Minute', 60), + ], + default=20, + ), + bs.IntChoiceSetting( + infection_spread_rate, + choices=[ + (slowest, 0.01), + (slow, 0.02), + (normal, 0.03), + (fast, 0.04), + (faster, 0.05), + (insane, 0.08), + ], + default=0.03, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.CoopSession) + or issubclass(sessiontype, bs.MultiTeamSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Doom Shroom', 'Rampage', 'Hockey Stadium', + 'Crag Castle', 'Big G', 'Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + self.mines: List = [] + self._update_rate = 0.1 + self._last_player_death_time = None + self._start_time: Optional[float] = None + self._timer: Optional[OnScreenTimer] = None + self._epic_mode = bool(settings['Epic Mode']) + self._max_mines = int(settings[mines]) + self._extra_mines = int(settings[extra_mines]) + self._enable_bombs = bool(settings[enable_bombs]) + self._max_size = int(settings[max_infected_size]) + self._max_size_increases = float(settings[max_size_increases]) + self._growth_rate = float(settings[infection_spread_rate]) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.SURVIVAL) + + def get_instance_description(self) -> Union[str, Sequence]: + return instance_description + + def get_instance_description_short(self) -> Union[str, Sequence]: + return instance_description + + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.mine_count = 0 + bs.timer(self._update_rate, + bs.WeakCall(self._mine_update), + repeat=True) + bs.timer(self._max_size_increases*1.0, + bs.WeakCall(self._max_size_update), + repeat=True) + bs.timer(self._extra_mines*1.0, + bs.WeakCall(self._max_mine_update), + repeat=True) + self._timer = OnScreenTimer() + self._timer.start() + + # Check for immediate end (if we've only got 1 player, etc). + bs.timer(5.0, self._check_end_game) + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + assert self._timer is not None + player.survival_seconds = self._timer.getstarttime() + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + return + self.spawn_player(player) + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + self._check_end_game() + + def _max_mine_update(self) -> None: + self._max_mines += 1 + + def _max_size_update(self) -> None: + self._max_size += 1 + + def _mine_update(self) -> None: + # print self.mineCount + # purge dead mines, update their animantion, check if players died + for m in self.mines: + if not m.node: + self.mines.remove(m) + else: + # First, check if any player is within the current death zone + for player in self.players: + if not player.actor is None: + if player.actor.is_alive(): + p1 = player.actor.node.position + p2 = m.node.position + diff = (babase.Vec3(p1[0]-p2[0], + 0.0, + p1[2]-p2[2])) + dist = (diff.length()) + if dist < m.rad: + player.actor.handlemessage(bs.DieMessage()) + # Now tell the circle to grow to the new size + if m.rad < self._max_size: + bs.animate_array( + m.zone, 'size', 1, + {0: [m.rad*2], + self._update_rate: [(m.rad+self._growth_rate)*2]}) + # Tell the circle to be the new size. + # This will be the new check radius next time. + m.rad += self._growth_rate + # make a new mine if needed. + if self.mine_count < self._max_mines: + pos = self.getRandomPowerupPoint() + self.mine_count += 1 + self._flash_mine(pos) + bs.timer(0.95, babase.Call(self._make_mine, pos)) + + def _make_mine(self, posn: Sequence[float]) -> None: + m = myMine(pos=posn) + m.arm() + self.mines.append(m) + + def _flash_mine(self, pos: Sequence[float]) -> None: + light = bs.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 0.2, 0.2), + 'radius': 0.1, + 'height_attenuated': False + }) + bs.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) + bs.timer(1.0, light.delete) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, int(team.survival_seconds)) + self.end(results=results, announce_delay=0.8) + + def _flash_player(self, player: Player, scale: float) -> None: + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + pos = player.actor.node.position + light = bs.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + bs.timer(0.5, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + death_time = bs.time() + msg.getplayer(Player).death_time = death_time + + if isinstance(self.session, bs.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + babase.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = death_time + else: + bs.timer(1.0, self._check_end_game) + + else: + # Default handler: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + # In co-op, we go till everyone is dead.. otherwise we go + # until one team remains. + if isinstance(self.session, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def spawn_player(self, player: PlayerType) -> bs.Actor: + spaz = self.spawn_player_spaz(player) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player(enable_punch=False, + enable_bomb=self._enable_bombs, + enable_pickup=False) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + def spawn_player_spaz(self, + player: PlayerType, + position: Sequence[float] = (0, 0, 0), + angle: float = None) -> PlayerSpaz: + """Create and wire up a bs.PlayerSpaz for the provided bs.Player.""" + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + position = self.map.get_ffa_start_position(self.players) + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = babase._math.normalized_color(color) + display_color = _babase.safecolor(color, target_intensity=0.75) + spaz = PlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + # If this is co-op and we're on Courtyard or Runaround, add the + # material that allows us to collide with the player-walls. + # FIXME: Need to generalize this. + if isinstance(self.session, bs.CoopSession) and self.map.getname() in [ + 'Courtyard', 'Tower D' + ]: + mat = self.map.preloaddata['collide_with_wall_material'] + assert isinstance(spaz.node.materials, tuple) + assert isinstance(spaz.node.roller_materials, tuple) + spaz.node.materials += (mat, ) + spaz.node.roller_materials += (mat, ) + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + return spaz + + def getRandomPowerupPoint(self) -> None: + # So far, randomized points only figured out for mostly rectangular maps. + # Boxes will still fall through holes, but shouldn't be terrible problem (hopefully) + # If you add stuff here, need to add to "supported maps" above. + # ['Doom Shroom', 'Rampage', 'Hockey Stadium', 'Courtyard', 'Crag Castle', 'Big G', 'Football Stadium'] + myMap = self.map.getname() + # print(myMap) + if myMap == 'Doom Shroom': + while True: + x = random.uniform(-1.0, 1.0) + y = random.uniform(-1.0, 1.0) + if x*x+y*y < 1.0: + break + return ((8.0*x, 2.5, -3.5+5.0*y)) + elif myMap == 'Rampage': + x = random.uniform(-6.0, 7.0) + y = random.uniform(-6.0, -2.5) + return ((x, 5.2, y)) + elif myMap == 'Hockey Stadium': + x = random.uniform(-11.5, 11.5) + y = random.uniform(-4.5, 4.5) + return ((x, 0.2, y)) + elif myMap == 'Courtyard': + x = random.uniform(-4.3, 4.3) + y = random.uniform(-4.4, 0.3) + return ((x, 3.0, y)) + elif myMap == 'Crag Castle': + x = random.uniform(-6.7, 8.0) + y = random.uniform(-6.0, 0.0) + return ((x, 10.0, y)) + elif myMap == 'Big G': + x = random.uniform(-8.7, 8.0) + y = random.uniform(-7.5, 6.5) + return ((x, 3.5, y)) + elif myMap == 'Football Stadium': + x = random.uniform(-12.5, 12.5) + y = random.uniform(-5.0, 5.5) + return ((x, 0.32, y)) + else: + x = random.uniform(-5.0, 5.0) + y = random.uniform(-6.0, 0.0) + return ((x, 8.0, y)) + + def end_game(self) -> None: + cur_time = bs.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + # Mark death-time as now for any still-living players + # and award players points for how long they lasted. + # (these per-player scores are only meaningful in team-games) + for team in self.teams: + for player in team.players: + survived = False + + # Throw an extra fudge factor in so teams that + # didn't die come out ahead of teams that did. + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + # Award a per-player score depending on how many seconds + # they lasted (per-player scores only affect teams mode; + # everywhere else just looks at the per-team score). + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 # A bit extra for survivors. + self.stats.player_scored(player, score, screenmessage=False) + + # Stop updating our time text, and set the final time to match + # exactly when our last guy died. + self._timer.stop(endtime=self._last_player_death_time) + + # Ok now calc game results: set a score for each team and then tell + # the game to end. + results = bs.GameResults() + + # Remember that 'free-for-all' mode is simply a special form + # of 'teams' mode where each player gets their own team, so we can + # just always deal in teams and have all cases covered. + for team in self.teams: + + # Set the team score to the max time survived by any player on + # that team. + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, + player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) diff --git a/plugins/minigames/infinite_ninjas.py b/plugins/minigames/infinite_ninjas.py new file mode 100644 index 000000000..6ff1b0a2b --- /dev/null +++ b/plugins/minigames/infinite_ninjas.py @@ -0,0 +1,139 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 + +# Copy pasted from ExplodoRun by Blitz,just edited Bots and map 😝 + + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazbot import SpazBotSet, ChargerBot, SpazBotDiedMessage +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Type, Dict, List, Optional + +## MoreMinigames.py support ## + + +def ba_get_api_version(): + return 8 + + +def ba_get_levels(): + return [bs._level.Level( + 'Infinite Ninjas', gametype=InfiniteNinjasGame, + settings={}, + preview_texture_name='footballStadiumPreview'), + bs._level.Level( + 'Epic Infinite Ninjas', gametype=InfiniteNinjasGame, + settings={'Epic Mode': True}, + preview_texture_name='footballStadiumPreview')] +## MoreMinigames.py support ## + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + +# ba_meta export bascenev1.GameActivity + + +class InfiniteNinjasGame(bs.TeamGameActivity[Player, Team]): + name = "Infinite Ninjas" + description = "How long can you survive from Ninjas??" + available_settings = [bs.BoolSetting('Epic Mode', default=False)] + scoreconfig = bs.ScoreConfig(label='Time', + scoretype=bs.ScoreType.MILLISECONDS, + lower_is_better=False) + default_music = bs.MusicType.TO_THE_DEATH + + def __init__(self, settings: dict): + settings['map'] = "Football Stadium" + self._epic_mode = settings.get('Epic Mode', False) + if self._epic_mode: + self.slow_motion = True + super().__init__(settings) + self._timer: Optional[OnScreenTimer] = None + self._winsound = bs.getsound('score') + self._won = False + self._bots = SpazBotSet() + self.wave = 1 + + def on_begin(self) -> None: + super().on_begin() + + self._timer = OnScreenTimer() + bs.timer(2.5, self._timer.start) + + # Bots Hehe + bs.timer(2.5, self.street) + + def street(self): + for a in range(self.wave): + p1 = random.choice([-5, -2.5, 0, 2.5, 5]) + p3 = random.choice([-4.5, -4.14, -5, -3]) + time = random.choice([1, 1.5, 2.5, 2]) + self._bots.spawn_bot(ChargerBot, pos=(p1, 0.4, p3), spawn_time=time) + self.wave += 1 + + def botrespawn(self): + if not self._bots.have_living_bots(): + self.street() + + def handlemessage(self, msg: Any) -> Any: + + # A player has died. + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard behavior. + self._won = True + self.end_game() + + # A spaz-bot has died. + elif isinstance(msg, SpazBotDiedMessage): + # Unfortunately the bot-set will always tell us there are living + # bots if we ask here (the currently-dying bot isn't officially + # marked dead yet) ..so lets push a call into the event loop to + # check once this guy has finished dying. + babase.pushcall(self.botrespawn) + + # Let the base class handle anything we don't. + else: + return super().handlemessage(msg) + return None + + # When this is called, we should fill out results and end the game + # *regardless* of whether is has been won. (this may be called due + # to a tournament ending or other external reason). + def end_game(self) -> None: + + # Stop our on-screen timer so players can see what they got. + assert self._timer is not None + self._timer.stop() + + results = bs.GameResults() + + # If we won, set our score to the elapsed time in milliseconds. + # (there should just be 1 team here since this is co-op). + # ..if we didn't win, leave scores as default (None) which means + # we lost. + if self._won: + elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0) + bs.cameraflash() + self._winsound.play() + for team in self.teams: + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage()) + results.set_team_score(team, elapsed_time_ms) + + # Ends the activity. + self.end(results) diff --git a/plugins/minigames/invisible_one.py b/plugins/minigames/invisible_one.py new file mode 100644 index 000000000..ad5676ce2 --- /dev/null +++ b/plugins/minigames/invisible_one.py @@ -0,0 +1,333 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# +# By itsre3 +# =>3<= +# Don't mind my spelling. i realized that they were not correct after making last change and saving +# Besides that, enjoy.......!! +"""Provides the chosen-one mini-game.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.flag import Flag +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Optional, Sequence, Union + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.chosen_light: Optional[bs.NodeActor] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self, time_remaining: int) -> None: + self.time_remaining = time_remaining + + +# ba_meta export bascenev1.GameActivity +class InvicibleOneGame(bs.TeamGameActivity[Player, Team]): + """ + Game involving trying to remain the one 'invisible one' + for a set length of time while everyone else tries to + kill you and become the invisible one themselves. + """ + + name = 'Invisible One' + description = ('Be the invisible one for a length of time to win.\n' + 'Kill the invisible one to become it.') + available_settings = [ + bs.IntSetting( + 'Invicible One Time', + min_value=10, + default=30, + increment=10, + ), + bs.BoolSetting('Invicible one is lazy', default=True), + bs.BoolSetting('Night mode', default=False), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + scoreconfig = bs.ScoreConfig(label='Time Held') + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('keep_away') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._invicible_one_player: Optional[Player] = None + self._swipsound = bs.getsound('swip') + self._countdownsounds: Dict[int, babase.Sound] = { + 10: bs.getsound('announceTen'), + 9: bs.getsound('announceNine'), + 8: bs.getsound('announceEight'), + 7: bs.getsound('announceSeven'), + 6: bs.getsound('announceSix'), + 5: bs.getsound('announceFive'), + 4: bs.getsound('announceFour'), + 3: bs.getsound('announceThree'), + 2: bs.getsound('announceTwo'), + 1: bs.getsound('announceOne') + } + self._flag_spawn_pos: Optional[Sequence[float]] = None + self._reset_region_material: Optional[bs.Material] = None + self._flag: Optional[Flag] = None + self._reset_region: Optional[bs.Node] = None + self._epic_mode = bool(settings['Epic Mode']) + self._invicible_one_time = int(settings['Invicible One Time']) + self._time_limit = float(settings['Time Limit']) + self._invicible_one_is_lazy = bool(settings['Invicible one is lazy']) + self._night_mode = bool(settings['Night mode']) + + # Base class overrides + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.CHOSEN_ONE) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Show your invisibility powers.' + + def create_team(self, sessionteam: bs.SessionTeam) -> Team: + return Team(time_remaining=self._invicible_one_time) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + if self._get_invicible_one_player() is player: + self._set_invicible_one_player(None) + + def on_begin(self) -> None: + super().on_begin() + shared = SharedObjects.get() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._flag_spawn_pos = self.map.get_flag_position(None) + Flag.project_stand(self._flag_spawn_pos) + self._set_invicible_one_player(None) + if self._night_mode: + gnode = bs.getactivity().globalsnode + gnode.tint = (0.4, 0.4, 0.4) + + pos = self._flag_spawn_pos + bs.timer(1.0, call=self._tick, repeat=True) + + mat = self._reset_region_material = bs.Material() + mat.add_actions( + conditions=( + 'they_have_material', + shared.player_material, + ), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + bs.WeakCall(self._handle_reset_collide)), + ), + ) + + self._reset_region = bs.newnode('region', + attrs={ + 'position': (pos[0], pos[1] + 0.75, + pos[2]), + 'scale': (0.5, 0.5, 0.5), + 'type': 'sphere', + 'materials': [mat] + }) + + def _get_invicible_one_player(self) -> Optional[Player]: + # Should never return invalid references; return None in that case. + if self._invicible_one_player: + return self._invicible_one_player + return None + + def _handle_reset_collide(self) -> None: + # If we have a chosen one, ignore these. + if self._get_invicible_one_player() is not None: + return + + # Attempt to get a Player controlling a Spaz that we hit. + try: + player = bs.getcollision().opposingnode.getdelegate( + PlayerSpaz, True).getplayer(Player, True) + except bs.NotFoundError: + return + + if player.is_alive(): + self._set_invicible_one_player(player) + + def _flash_flag_spawn(self) -> None: + light = bs.newnode('light', + attrs={ + 'position': self._flag_spawn_pos, + 'color': (1, 1, 1), + 'radius': 0.3, + 'height_attenuated': False + }) + bs.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) + + def _tick(self) -> None: + + # Give the chosen one points. + player = self._get_invicible_one_player() + if player is not None: + + # This shouldn't happen, but just in case. + if not player.is_alive(): + babase.print_error('got dead player as chosen one in _tick') + self._set_invicible_one_player(None) + else: + scoring_team = player.team + assert self.stats + self.stats.player_scored(player, + 3, + screenmessage=False, + display=False) + + scoring_team.time_remaining = max( + 0, scoring_team.time_remaining - 1) + + self._update_scoreboard() + + # announce numbers we have sounds for + if scoring_team.time_remaining in self._countdownsounds: + self._countdownsounds[scoring_team.time_remaining].play() + # Winner! + if scoring_team.time_remaining <= 0: + self.end_game() + + else: + # (player is None) + # This shouldn't happen, but just in case. + # (Chosen-one player ceasing to exist should + # trigger on_player_leave which resets chosen-one) + if self._invicible_one_player is not None: + babase.print_error('got nonexistent player as chosen one in _tick') + self._set_invicible_one_player(None) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, + self._invicible_one_time - team.time_remaining) + self.end(results=results, announce_delay=0) + + def _set_invicible_one_player(self, player: Optional[Player]) -> None: + existing = self._get_invicible_one_player() + if existing: + existing.chosen_light = None + self._swipsound.play() + if not player: + assert self._flag_spawn_pos is not None + self._flag = Flag(color=(1, 0.9, 0.2), + position=self._flag_spawn_pos, + touchable=False) + self._invicible_one_player = None + + # Create a light to highlight the flag; + # this will go away when the flag dies. + bs.newnode('light', + owner=self._flag.node, + attrs={ + 'position': self._flag_spawn_pos, + 'intensity': 0.6, + 'height_attenuated': False, + 'volume_intensity_scale': 0.1, + 'radius': 0.1, + 'color': (1.2, 1.2, 0.4) + }) + + # Also an extra momentary flash. + self._flash_flag_spawn() + else: + if player.actor: + self._flag = None + self._invicible_one_player = player + + if self._invicible_one_is_lazy: + player.actor.connect_controls_to_player( + enable_punch=False, enable_pickup=False, enable_bomb=False) + if player.actor.node.torso_mesh != None: + player.actor.node.color_mask_texture = None + player.actor.node.color_texture = None + player.actor.node.head_mesh = None + player.actor.node.torso_mesh = None + player.actor.node.upper_arm_mesh = None + player.actor.node.forearm_mesh = None + player.actor.node.pelvis_mesh = None + player.actor.node.toes_mesh = None + player.actor.node.upper_leg_mesh = None + player.actor.node.lower_leg_mesh = None + player.actor.node.hand_mesh = None + player.actor.node.style = 'cyborg' + invi_sound = [] + player.actor.node.jump_sounds = invi_sound + player.actor.attack_sounds = invi_sound + player.actor.impact_sounds = invi_sound + player.actor.pickup_sounds = invi_sound + player.actor.death_sounds = invi_sound + player.actor.fall_sounds = invi_sound + + player.actor.node.name = '' + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + if player is self._get_invicible_one_player(): + killerplayer = msg.getkillerplayer(Player) + self._set_invicible_one_player(None if ( + killerplayer is None or killerplayer is player + or not killerplayer.is_alive()) else killerplayer) + self.respawn_player(player) + else: + super().handlemessage(msg) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, + team.time_remaining, + self._invicible_one_time, + countdown=True) diff --git a/plugins/minigames/lame_fight.py b/plugins/minigames/lame_fight.py new file mode 100644 index 000000000..b9bce2102 --- /dev/null +++ b/plugins/minigames/lame_fight.py @@ -0,0 +1,169 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazbot import SpazBotSet, ChargerBot, BrawlerBotProShielded, TriggerBotProShielded, ExplodeyBot, BomberBotProShielded, SpazBotDiedMessage +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Type, Dict, List, Optional + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + +# ba_meta export bascenev1.GameActivity + + +class LameFightGame(bs.TeamGameActivity[Player, Team]): + name = "Lame Fight" + description = "Save World With Super Powers" + slow_motion = True + scoreconfig = bs.ScoreConfig(label='Time', + scoretype=bs.ScoreType.MILLISECONDS, + lower_is_better=True) + default_music = bs.MusicType.TO_THE_DEATH + + def __init__(self, settings: dict): + settings['map'] = "Courtyard" + super().__init__(settings) + self._timer: Optional[OnScreenTimer] = None + self._winsound = bs.getsound('score') + self._won = False + self._bots = SpazBotSet() + + def on_begin(self) -> None: + super().on_begin() + + self._timer = OnScreenTimer() + bs.timer(4.0, self._timer.start) + + # Bots Hehe + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(3, 3, -2), spawn_time=3.0)) + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(-3, 3, -2), spawn_time=3.0)) + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(5, 3, -2), spawn_time=3.0)) + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(-5, 3, -2), spawn_time=3.0)) + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(0, 3, 1), spawn_time=3.0)) + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(0, 3, -5), spawn_time=3.0)) + bs.timer(9.0, lambda: self._bots.spawn_bot( + BomberBotProShielded, pos=(-7, 5, -7.5), spawn_time=3.0)) + bs.timer(9.0, lambda: self._bots.spawn_bot( + BomberBotProShielded, pos=(7, 5, -7.5), spawn_time=3.0)) + bs.timer(9.0, lambda: self._bots.spawn_bot( + BomberBotProShielded, pos=(7, 5, 1.5), spawn_time=3.0)) + bs.timer(9.0, lambda: self._bots.spawn_bot( + BomberBotProShielded, pos=(-7, 5, 1.5), spawn_time=3.0)) + bs.timer(12.0, lambda: self._bots.spawn_bot( + TriggerBotProShielded, pos=(-1, 7, -8), spawn_time=3.0)) + bs.timer(12.0, lambda: self._bots.spawn_bot( + TriggerBotProShielded, pos=(1, 7, -8), spawn_time=3.0)) + bs.timer(15.0, lambda: self._bots.spawn_bot(ExplodeyBot, pos=(0, 3, -5), spawn_time=3.0)) + bs.timer(20.0, lambda: self._bots.spawn_bot(ExplodeyBot, pos=(0, 3, 1), spawn_time=3.0)) + bs.timer(20.0, lambda: self._bots.spawn_bot(ExplodeyBot, pos=(-5, 3, -2), spawn_time=3.0)) + bs.timer(20.0, lambda: self._bots.spawn_bot(ExplodeyBot, pos=(5, 3, -2), spawn_time=3.0)) + bs.timer(30, self.street) + + def street(self): + bs.broadcastmessage("Lame Guys Are Here!", color=(1, 0, 0)) + for a in range(-1, 2): + for b in range(-3, 0): + self._bots.spawn_bot(BrawlerBotProShielded, pos=(a, 3, b), spawn_time=3.0) + + def spawn_player(self, player: Player) -> bs.Actor: + spawn_center = (0, 3, -2) + pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], + spawn_center[2] + random.uniform(-1.5, 1.5)) + spaz = self.spawn_player_spaz(player, position=pos) + p = ["Bigger Blast", "Stronger Punch", "Shield", "Speed"] + Power = random.choice(p) + spaz.bomb_type = random.choice( + ["normal", "sticky", "ice", "impact", "normal", "ice", "sticky"]) + bs.broadcastmessage(f"Now You Have {Power}") + if Power == p[0]: + spaz.bomb_count = 3 + spaz.blast_radius = 2.5 + if Power == p[1]: + spaz._punch_cooldown = 350 + spaz._punch_power_scale = 2.0 + if Power == p[2]: + spaz.equip_shields() + if Power == p[3]: + spaz.node.hockey = True + return spaz + + def _check_if_won(self) -> None: + if not self._bots.have_living_bots(): + self._won = True + self.end_game() + + def handlemessage(self, msg: Any) -> Any: + + # A player has died. + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard behavior. + self.respawn_player(msg.getplayer(Player)) + + # A spaz-bot has died. + elif isinstance(msg, SpazBotDiedMessage): + # Unfortunately the bot-set will always tell us there are living + # bots if we ask here (the currently-dying bot isn't officially + # marked dead yet) ..so lets push a call into the event loop to + # check once this guy has finished dying. + babase.pushcall(self._check_if_won) + + # Let the base class handle anything we don't. + else: + return super().handlemessage(msg) + return None + + # When this is called, we should fill out results and end the game + # *regardless* of whether is has been won. (this may be called due + # to a tournament ending or other external reason). + def end_game(self) -> None: + + # Stop our on-screen timer so players can see what they got. + assert self._timer is not None + self._timer.stop() + + results = bs.GameResults() + + # If we won, set our score to the elapsed time in milliseconds. + # (there should just be 1 team here since this is co-op). + # ..if we didn't win, leave scores as default (None) which means + # we lost. + if self._won: + elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0) + bs.cameraflash() + self._winsound.play() + for team in self.teams: + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage()) + results.set_team_score(team, elapsed_time_ms) + + # Ends the activity. + self.end(results) + + +# ba_meta export plugin +class plugin(babase.Plugin): + def __init__(self): + ## Campaign support ## + babase.app.classic.add_coop_practice_level(bs.Level( + name='Lame Fight', + gametype=LameFightGame, + settings={}, + preview_texture_name='courtyardPreview')) diff --git a/plugins/minigames/laser_tracer.py b/plugins/minigames/laser_tracer.py new file mode 100644 index 000000000..e6e606785 --- /dev/null +++ b/plugins/minigames/laser_tracer.py @@ -0,0 +1,689 @@ + + +# Released under the MIT License. See LICENSE for details. +# https://youtu.be/wTgwZKiykQw?si=Cr0ybDYAcKCUNFN4 +# https://discord.gg/ucyaesh +# https://bombsquad-community.web.app/home +# by: Mr.Smoothy + +"""Elimination mini-game.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, Union +import random + + +class Icon(bs.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: tuple[float, float], + scale: float, + show_lives: bool = True, + show_death: bool = True, + name_scale: float = 1.0, + name_maxwidth: float = 115.0, + flatness: float = 1.0, + shadow: float = 1.0): + super().__init__() + + self._player = player + self._show_lives = show_lives + self._show_death = show_death + self._name_scale = name_scale + self._outline_tex = bs.gettexture('characterIconMask') + + icon = player.get_icon() + self.node = bs.newnode('image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + self._name_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': flatness, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + if self._show_lives: + self._lives_text = bs.newnode('text', + owner=self.node, + attrs={ + 'text': 'x0', + 'color': (1, 1, 0.5), + 'h_align': 'left', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_position_and_scale(position, scale) + + def set_position_and_scale(self, position: tuple[float, float], + scale: float) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + if self._show_lives: + self._lives_text.position = (position[0] + scale * 10.0, + position[1] - scale * 43.0) + self._lives_text.scale = 1.0 * scale + + def update_for_lives(self) -> None: + """Update for the target player's current lives.""" + if self._player: + lives = self._player.lives + else: + lives = 0 + if self._show_lives: + if lives > 0: + self._lives_text.text = 'x' + str(lives - 1) + else: + self._lives_text.text = '' + if lives == 0: + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + def handle_player_spawned(self) -> None: + """Our player spawned; hooray!""" + if not self.node: + return + self.node.opacity = 1.0 + self.update_for_lives() + + def handle_player_died(self) -> None: + """Well poo; our player died.""" + if not self.node: + return + if self._show_death: + bs.animate( + self.node, 'opacity', { + 0.00: 1.0, + 0.05: 0.0, + 0.10: 1.0, + 0.15: 0.0, + 0.20: 1.0, + 0.25: 0.0, + 0.30: 1.0, + 0.35: 0.0, + 0.40: 1.0, + 0.45: 0.0, + 0.50: 1.0, + 0.55: 0.2 + }) + lives = self._player.lives + if lives == 0: + bs.timer(0.6, self.update_for_lives) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + self.node.delete() + return None + return super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.lives = 0 + self.icons: list[Icon] = [] + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: Optional[int] = None + self.spawn_order: list[Player] = [] + + +# ba_meta export bascenev1.GameActivity +class LasorTracerGame(bs.TeamGameActivity[Player, Team]): + """Game type where last player(s) left alive win.""" + + name = 'Laser Tracer' + description = 'Last remaining alive wins.' + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, + none_is_winner=True) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Lives', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ["Courtyard"] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._start_time: Optional[float] = None + self._vs_text: Optional[bs.Actor] = None + self._round_end_timer: Optional[bs.Timer] = None + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = 1 + self._time_limit = float(settings['Time Limit']) + self._balance_total_lives = bool( + settings.get('Balance Total Lives', False)) + self._solo_mode = bool(settings.get('Solo Mode', False)) + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + self.laser_material = bs.Material() + self.laser_material.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=(('modify_part_collision', 'collide', True), + ('message', 'their_node', 'at_connect', bs.DieMessage())) + ) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Last team standing wins.' if isinstance( + self.session, bs.DualTeamSession) else 'Last one standing wins.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'last team standing wins' if isinstance( + self.session, bs.DualTeamSession) else 'last one standing wins' + + def on_player_join(self, player: Player) -> None: + player.lives = self._lives_per_player + + if self._solo_mode: + player.team.spawn_order.append(player) + self._update_solo_mode() + else: + # Create our icon and spawn. + # player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: + self.spawn_player(player) + + # Don't waste time doing this until begin. + if self.has_begun(): + self._update_icons() + + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + self.add_wall() + self.create_laser() + if self._solo_mode: + self._vs_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': babase.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, bs.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + bs.timer(1.0, self._update, repeat=True) + + def _update_solo_mode(self) -> None: + # For both teams, find the first player on the spawn order list with + # lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + break + + def _update_icons(self) -> None: + return + # lets do nothing ;Eat 5 Star + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon(player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False)) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def _get_spawn_point(self, player: Player) -> Optional[babase.Vec3]: + del player # Unused. + + # In solo-mode, if there's an existing live player on the map, spawn at + # whichever spot is farthest from them (keeps the action spread out). + if self._solo_mode: + living_player = None + living_player_pos = None + for team in self.teams: + for tplayer in team.players: + if tplayer.is_alive(): + assert tplayer.node + ppos = tplayer.node.position + living_player = tplayer + living_player_pos = ppos + break + if living_player: + assert living_player_pos is not None + player_pos = babase.Vec3(living_player_pos) + points: list[tuple[float, babase.Vec3]] = [] + for team in self.teams: + start_pos = babase.Vec3( + self.map.get_start_position(team.id)) + points.append( + ((start_pos - player_pos).length(), start_pos)) + # Hmm.. we need to sorting vectors too? + points.sort(key=lambda x: x[0]) + return points[-1][1] + return None + + def spawn_player(self, player: Player) -> bs.Actor: + actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) + actor.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_pickup=False) + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + return actor + + def _print_lives(self, player: Player) -> None: + from bascenev1lib.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + popuptext.PopupText('x' + str(player.lives - 1), + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=player.node.position).autoretain() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + player.icons = [] + + # Remove us from spawn-order. + if self._solo_mode: + if player in player.team.spawn_order: + player.team.spawn_order.remove(player) + + # Update icons in a moment since our team will be gone from the + # list then. + bs.timer(0, self._update_icons) + + # If the player to leave was the last in spawn order and had + # their final turn currently in-progress, mark the survival time + # for their team. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - self._start_time) + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + babase.print_error( + "Got lives < 0 in Elim; this shouldn't happen. solo:" + + str(self._solo_mode)) + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if self._solo_mode or player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + # If the whole team is now dead, mark their survival time. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - + self._start_time) + else: + # Otherwise, in regular mode, respawn. + if not self._solo_mode: + self.respawn_player(player) + + # In solo, put ourself at the back of the spawn order. + if self._solo_mode: + player.team.spawn_order.remove(player) + player.team.spawn_order.append(player) + + def _update(self) -> None: + if self._solo_mode: + # For both teams, find the first player on the spawn order + # list with lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + self._update_icons() + break + + # If we're down to 1 or fewer living teams, start a timer to end + # the game (allows the dust to settle and draws to occur if deaths + # are close enough). + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0.5, self.end_game) + + def _get_living_teams(self) -> list[Team]: + return [ + team for team in self.teams + if len(team.players) > 0 and any(player.lives > 0 + for player in team.players) + ] + + def end_game(self) -> None: + if self.has_ended(): + return + results = bs.GameResults() + self._vs_text = None # Kill our 'vs' if its there. + for team in self.teams: + results.set_team_score(team, team.survival_seconds) + self.end(results=results) + + def add_wall(self): + # FIXME: Chop this into vr and non-vr chunks. + shared = SharedObjects.get() + pwm = bs.Material() + cwwm = bs.Material() + pwm.add_actions( + actions=('modify_part_collision', 'friction', 0.0)) + # anything that needs to hit the wall should apply this. + + pwm.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=('modify_part_collision', 'collide', True)) + cmesh = bs.getcollisionmesh('courtyardPlayerWall') + self.player_wall = bs.newnode( + 'terrain', + attrs={ + 'collision_mesh': cmesh, + 'affect_bg_dynamics': False, + 'materials': [pwm] + }) + + def create_laser(self) -> None: + bs.timer(6, babase.Call(self.LRlaser, True)) + + bs.timer(7, babase.Call(self.UDlaser, True)) + bs.timer(30, babase.Call(self.create_laser)) + + def LRlaser(self, left): + ud_1_r = bs.newnode('region', attrs={'position': (-5, 2.6, 0), 'scale': ( + 0.1, 0.6, 15), 'type': 'box', 'materials': [self.laser_material]}) + shields = [] + x = -6 + for i in range(0, 30): + x = x+0.4 + node = bs.newnode('shield', owner=ud_1_r, attrs={ + 'color': (1, 0, 0), 'radius': 0.28}) + mnode = bs.newnode('math', + owner=ud_1_r, + attrs={ + 'input1': (0, 0.0, x), + 'operation': 'add' + }) + ud_1_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', node, 'position') + + _rcombine = bs.newnode('combine', + owner=ud_1_r, + attrs={ + 'input1': 2.6, + 'input2': -2, + 'size': 3 + }) + if left: + x1 = -10 + x2 = 10 + else: + x1 = 10 + x2 = -10 + bs.animate(_rcombine, 'input0', { + 0: x1, + 20: x2 + }) + _rcombine.connectattr('output', ud_1_r, 'position') + bs.timer(20, babase.Call(ud_1_r.delete)) + t = random.randrange(7, 13) + bs.timer(t, babase.Call(self.LRlaser, random.randrange(0, 2))) + + def UDlaser(self, up): + ud_2_r = bs.newnode('region', attrs={'position': (-3, 2.6, -6), 'scale': ( + 20, 0.6, 0.1), 'type': 'box', 'materials': [self.laser_material]}) + shields = [] + x = -6 + for i in range(0, 40): + x = x+0.4 + node = bs.newnode('shield', owner=ud_2_r, attrs={ + 'color': (1, 0, 0), 'radius': 0.28}) + mnode = bs.newnode('math', + owner=ud_2_r, + attrs={ + 'input1': (x, 0.0, 0), + 'operation': 'add' + }) + ud_2_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', node, 'position') + + _rcombine = bs.newnode('combine', + owner=ud_2_r, + attrs={ + 'input0': -2, + 'input1': 2.6, + 'size': 3 + }) + if up: + x1 = -9 + x2 = 6 + else: + x1 = 6 + x2 = -9 + bs.animate(_rcombine, 'input2', { + 0: x1, + 17: x2 + }) + _rcombine.connectattr('output', ud_2_r, 'position') + + bs.timer(17, babase.Call(ud_2_r.delete)) + t = random.randrange(6, 13) + bs.timer(t, babase.Call(self.UDlaser, random.randrange(0, 2))) diff --git a/plugins/minigames/last_punch_stand.py b/plugins/minigames/last_punch_stand.py new file mode 100644 index 000000000..75d3a4028 --- /dev/null +++ b/plugins/minigames/last_punch_stand.py @@ -0,0 +1,275 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +from typing import Sequence +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import random +from bascenev1lib.actor.spaz import Spaz +from bascenev1lib.actor.scoreboard import Scoreboard + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.score = 1 + + +class ChooseingSpazHitMessage: + def __init__(self, hitter: Player) -> None: + self.hitter = hitter + + +class ChooseingSpazDieMessage: + def __init__(self, killer: Player) -> None: + self.killer = killer + + +class ChooseingSpaz(Spaz): + def __init__( + self, + pos: Sequence[float], + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + ): + super().__init__(color, highlight, "Spaz", None, True, True, False, False) + self.last_player_attacked_by = None + self.stand(pos) + self.loc = bs.newnode( + 'locator', + attrs={ + 'shape': 'circleOutline', + 'position': pos, + 'color': color, + 'opacity': 1, + 'draw_beauty': False, + 'additive': True, + }, + ) + self.node.connectattr("position", self.loc, "position") + bs.animate_array(self.loc, "size", 1, keys={0: [0.5,], 1: [2,], 1.5: [0.5]}, loop=True) + + def handlemessage(self, msg): + if isinstance(msg, bs.FreezeMessage): + return + + if isinstance(msg, bs.PowerupMessage): + if not (msg.poweruptype == "health"): + return + + super().handlemessage(msg) + + if isinstance(msg, bs.HitMessage): + self.handlemessage(bs.PowerupMessage("health")) + + player = msg.get_source_player(Player) + if self.is_alive(): + self.activity.handlemessage(ChooseingSpazHitMessage(player)) + self.last_player_attacked_by = player + + elif isinstance(msg, bs.DieMessage): + player = self.last_player_attacked_by + + if msg.how.value != bs.DeathType.GENERIC.value: + self._dead = True + self.activity.handlemessage(ChooseingSpazDieMessage(player)) + + self.loc.delete() + + def stand(self, pos=(0, 0, 0), angle=0): + self.handlemessage(bs.StandMessage(pos, angle)) + + def recolor(self, color, highlight=(1, 1, 1)): + self.node.color = color + self.node.highlight = highlight + self.loc.color = color + + +class ChooseBilbord(bs.Actor): + def __init__(self, player: Player, delay=0.1) -> None: + super().__init__() + + icon = player.get_icon() + self.scale = 100 + + self.node = bs.newnode( + 'image', + delegate=self, + attrs={ + "position": (60, -125), + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'tint2_color': icon['tint2_color'], + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': "topLeft" + }, + ) + + self.name_node = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'position': (60, -185), + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'flatness': 1.0, + 'h_attach': 'left', + 'v_attach': 'top', + 'maxwidth': self.scale + }, + ) + + bs.animate_array(self.node, "scale", keys={ + 0 + delay: [0, 0], 0.05 + delay: [self.scale, self.scale]}, size=1) + bs.animate(self.name_node, "scale", {0 + delay: 0, 0.07 + delay: 1}) + + def handlemessage(self, msg): + super().handlemessage(msg) + if isinstance(msg, bs.DieMessage): + bs.animate_array(self.node, "scale", keys={0: self.node.scale, 0.05: [0, 0]}, size=1) + bs.animate(self.name_node, "scale", {0: self.name_node.scale, 0.07: 0}) + + def __delete(): + self.node.delete() + self.name_node.delete() + + bs.timer(0.2, __delete) + +# ba_meta export bascenev1.GameActivity + + +class LastPunchStand(bs.TeamGameActivity[Player, Team]): + name = "Last Punch Stand" + description = "Last one punchs the choosing spaz wins" + tips = [ + 'keep punching the choosing spaz to be last punched player at times up!', + 'you can not frezz the choosing spaz', + "evry time you punch the choosing spaz, you will get one point", + ] + + default_music = bs.MusicType.TO_THE_DEATH + + available_settings = [ + bs.FloatSetting("min time limit (in seconds)", 50.0, min_value=30.0), + bs.FloatSetting("max time limit (in seconds)", 160.0, 60), + + ] + + def __init__(self, settings: dict): + super().__init__(settings) + self._min_timelimit = settings["min time limit (in seconds)"] + self._max_timelimit = settings["max time limit (in seconds)"] + if (self._min_timelimit > self._max_timelimit): + self._max_timelimit = self._min_timelimit + + self._choosing_spaz_defcolor = (0.5, 0.5, 0.5) + self.choosing_spaz = None + self.choosed_player = None + self.times_uped = False + self.scoreboard = Scoreboard() + + def times_up(self): + self.times_uped = True + + for player in self.players: + if self.choosed_player and (player.team.id != self.choosed_player.team.id): + player.actor._cursed = True + player.actor.curse_explode() + + self.end_game() + + def __get_spaz_bot_spawn_point(self): + if len(self.map.tnt_points) > 0: + return self.map.tnt_points[random.randint(0, len(self.map.tnt_points)-1)] + else: + return (0, 6, 0) + + def spaw_bot(self): + "spawns a choosing bot" + + self.choosing_spaz = ChooseingSpaz(self.__get_spaz_bot_spawn_point()) + self.choose_bilbord = None + + def on_begin(self) -> None: + super().on_begin() + time_limit = random.randint(self._min_timelimit, self._max_timelimit) + self.spaw_bot() + bs.timer(time_limit, self.times_up) + + self.setup_standard_powerup_drops(False) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + if self.choosed_player and (team.id == self.choosed_player.team.id): + team.score += 100 + results.set_team_score(team, team.score) + self.end(results=results) + + def change_choosed_player(self, hitter: Player): + if hitter: + self.choosing_spaz.recolor(hitter.color, hitter.highlight) + self.choosed_player = hitter + hitter.team.score += 1 + self.choose_bilbord = ChooseBilbord(hitter) + self.hide_score_board() + else: + self.choosing_spaz.recolor(self._choosing_spaz_defcolor) + self.choosed_player = None + self.choose_bilbord = None + self.show_score_board() + + def show_score_board(self): + self.scoreboard = Scoreboard() + for team in self.teams: + self.scoreboard.set_team_value(team, team.score) + + def hide_score_board(self): + self.scoreboard = None + + def _watch_dog_(self): + "checks if choosing spaz exists" + # choosing spaz wont respawn if death type if generic + # this becuse we dont want to keep respawn him when he dies because of losing referce + # but sometimes "choosing spaz" dies naturaly and his death type is generic! so it wont respawn back again + # thats why we have this function; to check if spaz exits in the case that he didnt respawned + + if self.choosing_spaz: + if self.choosing_spaz._dead: + self.spaw_bot() + else: + self.spaw_bot() + + def handlemessage(self, msg): + super().handlemessage(msg) + + if isinstance(msg, ChooseingSpazHitMessage): + hitter = msg.hitter + if self.choosing_spaz.node and hitter: + self.change_choosed_player(hitter) + + elif isinstance(msg, ChooseingSpazDieMessage): + self.spaw_bot() + self.change_choosed_player(None) + + elif isinstance(msg, bs.PlayerDiedMessage): + player = msg.getplayer(Player) + if not (self.has_ended() or self.times_uped): + self.respawn_player(player, 0) + + if self.choosed_player and (player.getname(True) == self.choosed_player.getname(True)): + self.change_choosed_player(None) + + self._watch_dog_() diff --git a/plugins/minigames/memory_game.py b/plugins/minigames/memory_game.py new file mode 100644 index 000000000..4d21c8968 --- /dev/null +++ b/plugins/minigames/memory_game.py @@ -0,0 +1,1003 @@ +from __future__ import annotations + + +## Original creator: byANG3L ## +## Made by: Freaku ## + +## From: BSWorld Modpack (https://youtu.be/1TN56NLlShE) ## + + +# Used in-game boxes and textures instead of external +# So it will run on server and randoms can play init ._. +# (& some improvements) + + +# incase someone is wondering how is map floating. Check out +# def spawnAllMap(self) + + +# ba_meta require api 9 +from typing import TYPE_CHECKING, overload +import _babase +import babase +import random +import bascenev1 as bs +from bascenev1lib.gameutils import SharedObjects +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List, Dict, Type, Union, Any, Literal + + +class OnTimer(bs.Actor): + """Timer which counts but doesn't show on-screen""" + + def __init__(self) -> None: + super().__init__() + self._starttime_ms: int | None = None + self.node = bs.newnode('text', attrs={'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'color': ( + 1, 1, 0.5, 1), 'flatness': 0.5, 'shadow': 0.5, 'position': (0, -70), 'scale': 0, 'text': ''}) + self.inputnode = bs.newnode( + 'timedisplay', attrs={'timemin': 0, 'showsubseconds': True} + ) + self.inputnode.connectattr('output', self.node, 'text') + + def start(self) -> None: + """Start the timer.""" + tval = int(bs.time() * 1000.0) + assert isinstance(tval, int) + self._starttime_ms = tval + self.inputnode.time1 = self._starttime_ms + bs.getactivity().globalsnode.connectattr( + 'time', self.inputnode, 'time2' + ) + + def has_started(self) -> bool: + """Return whether this timer has started yet.""" + return self._starttime_ms is not None + + def stop(self, endtime: int | float | None = None) -> None: + """End the timer. + + If 'endtime' is not None, it is used when calculating + the final display time; otherwise the current time is used. + """ + if endtime is None: + endtime = bs.time() + + if self._starttime_ms is None: + logging.warning( + 'OnScreenTimer.stop() called without first calling start()' + ) + else: + endtime_ms = int(endtime * 1000) + self.inputnode.timemax = endtime_ms - self._starttime_ms + + def getstarttime(self) -> float: + """Return the scene-time when start() was called. + + Time will be returned in seconds if timeformat is SECONDS or + milliseconds if it is MILLISECONDS. + """ + val_ms: Any + if self._starttime_ms is None: + print('WARNING: getstarttime() called on un-started timer') + val_ms = int(bs.time() * 1000.0) + else: + val_ms = self._starttime_ms + assert isinstance(val_ms, int) + return 0.001 * val_ms + + @property + def starttime(self) -> float: + """Shortcut for start time in seconds.""" + return self.getstarttime() + + def handlemessage(self, msg: Any) -> Any: + # if we're asked to die, just kill our node/timer + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class MGgame(bs.TeamGameActivity[Player, Team]): + + name = 'Memory Game' + description = 'Memories tiles and survive till the end!' + available_settings = [bs.BoolSetting( + 'Epic Mode', default=False), bs.BoolSetting('Enable Bottom Credits', True)] + scoreconfig = bs.ScoreConfig(label='Survived', scoretype=bs.ScoreType.MILLISECONDS, version='B') + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + + # we're currently hard-coded for one map.. + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Sky Tiles'] + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession) + or issubclass(sessiontype, babase.CoopSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + + self._epic_mode = settings.get('Epic Mode', False) + self._last_player_death_time: Optional[float] = None + self._timer: Optional[OnTimer] = None + self.credit_text = bool(settings['Enable Bottom Credits']) + + # Some base class overrides: + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + if self._epic_mode: + self.slow_motion = True + shared = SharedObjects.get() + self._collide_with_player = bs.Material() + self._collide_with_player.add_actions(actions=(('modify_part_collision', 'collide', True))) + self.dont_collide = bs.Material() + self.dont_collide.add_actions(actions=(('modify_part_collision', 'collide', False))) + self._levelStage = 0 + + self.announcePlayerDeaths = True + self._lastPlayerDeathTime = None + self._spawnCenter = (-3.17358, 2.75764, -2.99124) + + self._mapFGPModel = bs.getmesh('buttonSquareOpaque') + self._mapFGPDefaultTex = bs.gettexture('achievementOffYouGo') + + self._mapFGCurseTex = bs.gettexture('powerupCurse') + self._mapFGHealthTex = bs.gettexture('powerupHealth') + self._mapFGIceTex = bs.gettexture('powerupIceBombs') + self._mapFGImpactTex = bs.gettexture('powerupImpactBombs') + self._mapFGMinesTex = bs.gettexture('powerupLandMines') + self._mapFGPunchTex = bs.gettexture('powerupPunch') + self._mapFGShieldTex = bs.gettexture('powerupShield') + self._mapFGStickyTex = bs.gettexture('powerupStickyBombs') + + self._mapFGSpaz = bs.gettexture('neoSpazIcon') + self._mapFGZoe = bs.gettexture('zoeIcon') + self._mapFGSnake = bs.gettexture('ninjaIcon') + self._mapFGKronk = bs.gettexture('kronkIcon') + self._mapFGMel = bs.gettexture('melIcon') + self._mapFGJack = bs.gettexture('jackIcon') + self._mapFGSanta = bs.gettexture('santaIcon') + self._mapFGFrosty = bs.gettexture('frostyIcon') + self._mapFGBones = bs.gettexture('bonesIcon') + self._mapFGBernard = bs.gettexture('bearIcon') + self._mapFGPascal = bs.gettexture('penguinIcon') + self._mapFGAli = bs.gettexture('aliIcon') + self._mapFGRobot = bs.gettexture('cyborgIcon') + self._mapFGAgent = bs.gettexture('agentIcon') + self._mapFGGrumbledorf = bs.gettexture('wizardIcon') + self._mapFGPixel = bs.gettexture('pixieIcon') + + self._imageTextDefault = bs.gettexture('bg') + self._circleTex = bs.gettexture('circleShadow') + + self._image = bs.newnode('image', + attrs={'texture': self._imageTextDefault, + 'position': (0, -100), + 'scale': (100, 100), + 'opacity': 0.0, + 'attach': 'topCenter'}) + + self._textCounter = bs.newnode('text', + attrs={'text': '10', + 'position': (0, -100), + 'scale': 2.3, + 'shadow': 1.0, + 'flatness': 1.0, + 'opacity': 0.0, + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'v_align': 'center'}) + + self._textLevel = bs.newnode('text', + attrs={'text': 'Level ' + str(self._levelStage), + 'position': (0, -28), + 'scale': 1.3, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (1.0, 0.0, 1.0), + 'opacity': 0.0, + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'v_align': 'center'}) + + self._imageCircle = bs.newnode('image', + attrs={'texture': self._circleTex, + 'position': (75, -75), + 'scale': (20, 20), + 'color': (0.2, 0.2, 0.2), + 'opacity': 0.0, + 'attach': 'topCenter'}) + self._imageCircle2 = bs.newnode('image', + attrs={'texture': self._circleTex, + 'position': (75, -100), + 'scale': (20, 20), + 'color': (0.2, 0.2, 0.2), + 'opacity': 0.0, + 'attach': 'topCenter'}) + self._imageCircle3 = bs.newnode('image', + attrs={'texture': self._circleTex, + 'position': (75, -125), + 'scale': (20, 20), + 'color': (0.2, 0.2, 0.2), + 'opacity': 0.0, + 'attach': 'topCenter'}) + + def on_transition_in(self) -> None: + super().on_transition_in() + self._bellLow = bs.getsound('bellLow') + self._bellMed = bs.getsound('bellMed') + self._bellHigh = bs.getsound('bellHigh') + self._tickSound = bs.getsound('tick') + self._tickFinal = bs.getsound('powerup01') + self._scoreSound = bs.getsound('score') + + self._image.opacity = 1 + self._textCounter.opacity = 1 + self._textLevel.opacity = 1 + self._imageCircle.opacity = 0.7 + self._imageCircle2.opacity = 0.7 + self._imageCircle3.opacity = 0.7 + + self._levelStage += 1 + + self._textLevel.text = 'Level ' + str(self._levelStage) + + self._image.texture = self._imageTextDefault + + if self._levelStage == 1: + timeStart = 6 + bs.timer(timeStart, self._randomPlatform) + bs.timer(timeStart, self.startCounter) + + def on_begin(self) -> None: + super().on_begin() + + self._timer = OnTimer() + self._timer.start() + + self.coldel = True + self.coldel2 = True + self.coldel3 = True + self.coldel4 = True + self.coldel5 = True + self.coldel6 = True + self.coldel7 = True + self.coldel8 = True + self.coldel9 = True + self.coldel10 = True + self.coldel11 = True + self.coldel12 = True + self.coldel13 = True + self.coldel14 = True + self.coldel15 = True + self.coldel16 = True + if self.credit_text: + t = bs.newnode('text', + attrs={'text': "Made by Freaku\nOriginally for 1.4: byANG3L", # Disable 'Enable Bottom Credits' when making playlist, No need to edit this lovely... + 'scale': 0.7, + 'position': (0, 0), + 'shadow': 0.5, + 'flatness': 1.2, + 'color': (1, 1, 1), + 'h_align': 'center', + 'v_attach': 'bottom'}) + self.spawnAllMap() + self.flashHide() + + # Check for immediate end (if we've only got 1 player, etc). + bs.timer(5, self._check_end_game) + self._dingSound = bs.getsound('dingSmall') + self._dingSoundHigh = bs.getsound('dingSmallHigh') + + def startCounter(self): + self._textCounter.text = '10' + + def count9(): + def count8(): + def count7(): + def count6(): + def count5(): + def count4(): + def count3(): + def count2(): + def count1(): + def countFinal(): + self._textCounter.text = '' + self._tickFinal.play() + self._stop() + self._textCounter.text = '1' + self._tickSound.play() + bs.timer(1, countFinal) + self._textCounter.text = '2' + self._tickSound.play() + bs.timer(1, count1) + self._textCounter.text = '3' + self._tickSound.play() + bs.timer(1, count2) + self._textCounter.text = '4' + self._tickSound.play() + bs.timer(1, count3) + self._textCounter.text = '5' + self._tickSound.play() + bs.timer(1, count4) + self._textCounter.text = '6' + self._tickSound.play() + bs.timer(1, count5) + self._textCounter.text = '7' + self._tickSound.play() + bs.timer(1, count6) + self._textCounter.text = '8' + self._tickSound.play() + bs.timer(1, count7) + self._textCounter.text = '9' + self._tickSound.play() + bs.timer(1, count8) + bs.timer(1, count9) + + def on_player_join(self, player: Player) -> None: + # Don't allow joining after we start + # (would enable leave/rejoin tomfoolery). + if self.has_begun(): + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), transient=True, clients=[player.sessionplayer.inputdevice.client_id]) + # For score purposes, mark them as having died right as the + # game started. + assert self._timer is not None + player.death_time = self._timer.getstarttime() + return + self.spawn_player(player) + + def on_player_leave(self, player: Player) -> None: + # Augment default behavior. + super().on_player_leave(player) + + # A departing player may trigger game-over. + self._check_end_game() + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> bs.Actor: + spaz = self.spawn_player_spaz(player) + pos = (self._spawnCenter[0] + random.uniform(-1.5, 2.5), + self._spawnCenter[1], self._spawnCenter[2] + random.uniform(-2.5, 1.5)) + spaz.connect_controls_to_player(enable_punch=False, enable_bomb=False, enable_pickup=False) + spaz.handlemessage(bs.StandMessage(pos)) + return spaz + + def _randomSelect(self): + if self._levelStage == 1: + self._textureSelected = random.choice([self._mapFGMinesTex, + self._mapFGStickyTex]) + self._image.texture = self._textureSelected + elif self._levelStage == 2: + self._textureSelected = random.choice([self._mapFGIceTex, + self._mapFGShieldTex]) + self._image.texture = self._textureSelected + elif self._levelStage in [3, 4, 5]: + self._textureSelected = random.choice([self._mapFGStickyTex, + self._mapFGIceTex, + self._mapFGImpactTex, + self._mapFGMinesTex]) + self._image.texture = self._textureSelected + elif self._levelStage in [6, 7, 8, 9]: + self._textureSelected = random.choice([self._mapFGCurseTex, + self._mapFGHealthTex, + self._mapFGIceTex, + self._mapFGImpactTex, + self._mapFGMinesTex, + self._mapFGPunchTex, + self._mapFGShieldTex]) + self._image.texture = self._textureSelected + elif self._levelStage >= 10: + self._textureSelected = random.choice([self._mapFGSpaz, + self._mapFGZoe, + self._mapFGSnake, + self._mapFGKronk, + self._mapFGMel, + self._mapFGJack, + self._mapFGSanta, + self._mapFGFrosty, + self._mapFGBones, + self._mapFGBernard, + self._mapFGPascal, + self._mapFGAli, + self._mapFGRobot, + self._mapFGAgent, + self._mapFGGrumbledorf, + self._mapFGPixel]) + self._image.texture = self._textureSelected + return self._textureSelected + + def _stop(self): + self._textureSelected = self._randomSelect() + + def circle(): + def circle2(): + def circle3(): + self._imageCircle3.color = (0.0, 1.0, 0.0) + self._imageCircle3.opacity = 1.0 + self._bellHigh.play() + bs.timer(0.2, self._doDelete) + self._imageCircle2.color = (1.0, 1.0, 0.0) + self._imageCircle2.opacity = 1.0 + self._bellMed.play() + bs.timer(1, circle3) + self._imageCircle.color = (1.0, 0.0, 0.0) + self._imageCircle.opacity = 1.0 + self._bellLow.play() + bs.timer(1, circle2) + bs.timer(1, circle) + + def _randomPlatform(self): + if self._levelStage == 1: + randomTexture = [self._mapFGMinesTex, + self._mapFGMinesTex, + self._mapFGMinesTex, + self._mapFGMinesTex, + self._mapFGMinesTex, + self._mapFGMinesTex, + self._mapFGMinesTex, + self._mapFGMinesTex, + self._mapFGStickyTex, + self._mapFGStickyTex, + self._mapFGStickyTex, + self._mapFGStickyTex, + self._mapFGStickyTex, + self._mapFGStickyTex, + self._mapFGStickyTex, + self._mapFGStickyTex] + elif self._levelStage == 2: + randomTexture = [self._mapFGIceTex, + self._mapFGIceTex, + self._mapFGIceTex, + self._mapFGIceTex, + self._mapFGIceTex, + self._mapFGIceTex, + self._mapFGIceTex, + self._mapFGIceTex, + self._mapFGShieldTex, + self._mapFGShieldTex, + self._mapFGShieldTex, + self._mapFGShieldTex, + self._mapFGShieldTex, + self._mapFGShieldTex, + self._mapFGShieldTex, + self._mapFGShieldTex] + elif self._levelStage in [3, 4, 5]: + randomTexture = [self._mapFGStickyTex, + self._mapFGStickyTex, + self._mapFGStickyTex, + self._mapFGStickyTex, + self._mapFGIceTex, + self._mapFGIceTex, + self._mapFGIceTex, + self._mapFGIceTex, + self._mapFGImpactTex, + self._mapFGImpactTex, + self._mapFGImpactTex, + self._mapFGImpactTex, + self._mapFGMinesTex, + self._mapFGMinesTex, + self._mapFGMinesTex, + self._mapFGMinesTex] + elif self._levelStage in [6, 7, 8, 9]: + randomTexture = [self._mapFGHealthTex, + self._mapFGShieldTex, + self._mapFGCurseTex, + self._mapFGCurseTex, + self._mapFGHealthTex, + self._mapFGHealthTex, + self._mapFGIceTex, + self._mapFGIceTex, + self._mapFGImpactTex, + self._mapFGImpactTex, + self._mapFGMinesTex, + self._mapFGMinesTex, + self._mapFGPunchTex, + self._mapFGPunchTex, + self._mapFGShieldTex, + self._mapFGShieldTex] + elif self._levelStage >= 10: + randomTexture = [self._mapFGSpaz, + self._mapFGZoe, + self._mapFGSnake, + self._mapFGKronk, + self._mapFGMel, + self._mapFGJack, + self._mapFGSanta, + self._mapFGFrosty, + self._mapFGBones, + self._mapFGBernard, + self._mapFGPascal, + self._mapFGAli, + self._mapFGRobot, + self._mapFGAgent, + self._mapFGGrumbledorf, + self._mapFGPixel] + + (self.mapFGPTex, self.mapFGP2Tex, + self.mapFGP3Tex, self.mapFGP4Tex, + self.mapFGP5Tex, self.mapFGP6Tex, + self.mapFGP7Tex, self.mapFGP8Tex, + self.mapFGP9Tex, self.mapFGP10Tex, + self.mapFGP11Tex, self.mapFGP12Tex, + self.mapFGP13Tex, self.mapFGP14Tex, + self.mapFGP15Tex, self.mapFGP16Tex) = ( + random.sample(randomTexture, 16)) + self._mixPlatform() + + def _mixPlatform(self): + bs.timer(1, self.flashShow) + bs.timer(3, self.flashHide) + bs.timer(4, self.flashShow) + bs.timer(6, self.flashHide) + bs.timer(7, self.flashShow) + bs.timer(9, self.flashHide) + bs.timer(13.2, self.flashShow) + + def flashHide(self): + self.mapFGP.color_texture = self._mapFGPDefaultTex + self.mapFGP2.color_texture = self._mapFGPDefaultTex + self.mapFGP3.color_texture = self._mapFGPDefaultTex + self.mapFGP4.color_texture = self._mapFGPDefaultTex + self.mapFGP5.color_texture = self._mapFGPDefaultTex + self.mapFGP6.color_texture = self._mapFGPDefaultTex + self.mapFGP7.color_texture = self._mapFGPDefaultTex + self.mapFGP8.color_texture = self._mapFGPDefaultTex + self.mapFGP9.color_texture = self._mapFGPDefaultTex + self.mapFGP10.color_texture = self._mapFGPDefaultTex + self.mapFGP11.color_texture = self._mapFGPDefaultTex + self.mapFGP12.color_texture = self._mapFGPDefaultTex + self.mapFGP13.color_texture = self._mapFGPDefaultTex + self.mapFGP14.color_texture = self._mapFGPDefaultTex + self.mapFGP15.color_texture = self._mapFGPDefaultTex + self.mapFGP16.color_texture = self._mapFGPDefaultTex + + def flashShow(self): + self.mapFGP.color_texture = self.mapFGPTex + self.mapFGP2.color_texture = self.mapFGP2Tex + self.mapFGP3.color_texture = self.mapFGP3Tex + self.mapFGP4.color_texture = self.mapFGP4Tex + self.mapFGP5.color_texture = self.mapFGP5Tex + self.mapFGP6.color_texture = self.mapFGP6Tex + self.mapFGP7.color_texture = self.mapFGP7Tex + self.mapFGP8.color_texture = self.mapFGP8Tex + self.mapFGP9.color_texture = self.mapFGP9Tex + self.mapFGP10.color_texture = self.mapFGP10Tex + self.mapFGP11.color_texture = self.mapFGP11Tex + self.mapFGP12.color_texture = self.mapFGP12Tex + self.mapFGP13.color_texture = self.mapFGP13Tex + self.mapFGP14.color_texture = self.mapFGP14Tex + self.mapFGP15.color_texture = self.mapFGP15Tex + self.mapFGP16.color_texture = self.mapFGP16Tex + + def _doDelete(self): + if not self.mapFGPTex == self._textureSelected: + self.mapFGP.delete() + self.mapFGPcol.delete() + self.coldel = True + if not self.mapFGP2Tex == self._textureSelected: + self.mapFGP2.delete() + self.mapFGP2col.delete() + self.coldel2 = True + if not self.mapFGP3Tex == self._textureSelected: + self.mapFGP3.delete() + self.mapFGP3col.delete() + self.coldel3 = True + if not self.mapFGP4Tex == self._textureSelected: + self.mapFGP4.delete() + self.mapFGP4col.delete() + self.coldel4 = True + if not self.mapFGP5Tex == self._textureSelected: + self.mapFGP5.delete() + self.mapFGP5col.delete() + self.coldel5 = True + if not self.mapFGP6Tex == self._textureSelected: + self.mapFGP6.delete() + self.mapFGP6col.delete() + self.coldel6 = True + if not self.mapFGP7Tex == self._textureSelected: + self.mapFGP7.delete() + self.mapFGP7col.delete() + self.coldel7 = True + if not self.mapFGP8Tex == self._textureSelected: + self.mapFGP8.delete() + self.mapFGP8col.delete() + self.coldel8 = True + if not self.mapFGP9Tex == self._textureSelected: + self.mapFGP9.delete() + self.mapFGP9col.delete() + self.coldel9 = True + if not self.mapFGP10Tex == self._textureSelected: + self.mapFGP10.delete() + self.mapFGP10col.delete() + self.coldel10 = True + if not self.mapFGP11Tex == self._textureSelected: + self.mapFGP11.delete() + self.mapFGP11col.delete() + self.coldel11 = True + if not self.mapFGP12Tex == self._textureSelected: + self.mapFGP12.delete() + self.mapFGP12col.delete() + self.coldel12 = True + if not self.mapFGP13Tex == self._textureSelected: + self.mapFGP13.delete() + self.mapFGP13col.delete() + self.coldel13 = True + if not self.mapFGP14Tex == self._textureSelected: + self.mapFGP14.delete() + self.mapFGP14col.delete() + self.coldel14 = True + if not self.mapFGP15Tex == self._textureSelected: + self.mapFGP15.delete() + self.mapFGP15col.delete() + self.coldel15 = True + if not self.mapFGP16Tex == self._textureSelected: + self.mapFGP16.delete() + self.mapFGP16col.delete() + self.coldel16 = True + + bs.timer(3.3, self._platformTexDefault) + + def spawnAllMap(self): + """ + # Here's how it works: + # First, create prop with a gravity scale of 0 + # Then use a in-game mesh which will suit it (For this one I didn't chose box, since it will look kinda weird) Right? + # Instead I used a 2d mesh (which is nothing but a button in menu) + # This prop SHOULD NOT collide with anything, since it has gravity_scale of 0 if it'll get weight it will fall down :(( + # These are where we change those color-textures and is seen in-game + + # Now lets talk about the actual node on which we stand (sadly no-one realises it exists) + # A moment of silence for this node... + + # Alright, so this is a region node (the one used in hockey/football for scoring) + # Thanksfully these are just thicc boxes positioned on the map (so they are not moved neither they have gravity_scale) + # So we create this region node and place it to the same position of our prop node + # and give it collide_with_player and footing materials + # Thats it, now you have your own floating platforms :D + """ + shared = SharedObjects.get() + if self.coldel: + self.mapFGP = bs.newnode('prop', + attrs={'body': 'puck', 'position': (4.5, 2, -9), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGPTex = None + self.mapFGPcol = bs.newnode('region', attrs={'position': (4.5, 2, -9), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel = False + + if self.coldel2: + self.mapFGP2 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (4.5, 2, -6), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP2Tex = None + self.mapFGP2col = bs.newnode('region', attrs={'position': (4.5, 2, -6), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel2 = False + + if self.coldel3: + self.mapFGP3 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (4.5, 2, -3), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP3Tex = None + self.mapFGP3col = bs.newnode('region', attrs={'position': (4.5, 2, -3), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel3 = False + + if self.coldel4: + self.mapFGP4 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (4.5, 2, 0), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP4Tex = None + self.mapFGP4col = bs.newnode('region', attrs={'position': (4.5, 2, 0), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel4 = False + + if self.coldel5: + self.mapFGP5 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (1.5, 2, -9), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP5Tex = None + self.mapFGP5col = bs.newnode('region', attrs={'position': (1.5, 2, -9), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel5 = False + + if self.coldel6: + self.mapFGP6 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (1.5, 2, -6), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP6Tex = None + self.mapFGP6col = bs.newnode('region', attrs={'position': (1.5, 2, -6), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel6 = False + + if self.coldel7: + self.mapFGP7 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (1.5, 2, -3), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP7Tex = None + self.mapFGP7col = bs.newnode('region', attrs={'position': (1.5, 2, -3), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel7 = False + + if self.coldel8: + self.mapFGP8 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (1.5, 2, 0), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP8Tex = None + self.mapFGP8col = bs.newnode('region', attrs={'position': (1.5, 2, 0), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel8 = False + + if self.coldel9: + self.mapFGP9 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (-1.5, 2, -9), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP9Tex = None + self.mapFGP9col = bs.newnode('region', attrs={'position': (-1.5, 2, -9), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel9 = False + + if self.coldel10: + self.mapFGP10 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (-1.5, 2, -6), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP10Tex = None + self.mapFGP10col = bs.newnode('region', attrs={'position': (-1.5, 2, -6), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel10 = False + + if self.coldel11: + self.mapFGP11 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (-1.5, 2, -3), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP11Tex = None + self.mapFGP11col = bs.newnode('region', attrs={'position': (-1.5, 2, -3), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel11 = False + + if self.coldel12: + self.mapFGP12 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (-1.5, 2, 0), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP12Tex = None + self.mapFGP12col = bs.newnode('region', attrs={'position': (-1.5, 2, 0), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel12 = False + + if self.coldel13: + self.mapFGP13 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (-4.5, 2, -9), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP13Tex = None + self.mapFGP13col = bs.newnode('region', attrs={'position': (-4.5, 2, -9), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel13 = False + + if self.coldel14: + self.mapFGP14 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (-4.5, 2, -6), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP14Tex = None + self.mapFGP14col = bs.newnode('region', attrs={'position': (-4.5, 2, -6), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel14 = False + + if self.coldel15: + self.mapFGP15 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (-4.5, 2, -3), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP15Tex = None + self.mapFGP15col = bs.newnode('region', attrs={'position': (-4.5, 2, -3), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel15 = False + + if self.coldel16: + self.mapFGP16 = bs.newnode('prop', + attrs={'body': 'puck', 'position': (-4.5, 2, 0), 'mesh': self._mapFGPModel, 'mesh_scale': 3.73, 'body_scale': 3.73, 'shadow_size': 0.5, 'gravity_scale': 0.0, 'color_texture': self._mapFGPDefaultTex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mapFGP16Tex = None + self.mapFGP16col = bs.newnode('region', attrs={'position': (-4.5, 2, 0), 'scale': ( + 3.5, 0.1, 3.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + self.coldel16 = False + + def _platformTexDefault(self): + self._textureSelected = None + + self._imageCircle.color = (0.2, 0.2, 0.2) + self._imageCircle.opacity = 0.7 + self._imageCircle2.color = (0.2, 0.2, 0.2) + self._imageCircle2.opacity = 0.7 + self._imageCircle3.color = (0.2, 0.2, 0.2) + self._imageCircle3.opacity = 0.7 + + self._levelStage += 1 + + self._textLevel.text = 'Level ' + str(self._levelStage) + + self._image.texture = self._imageTextDefault + + if self._levelStage == 1: + timeStart = 6 + else: + timeStart = 2 + self._scoreSound.play() + activity = bs.get_foreground_host_activity() + for i in activity.players: + try: + i.actor.node.handlemessage(bs.CelebrateMessage(2.0)) + except: + pass + bs.timer(timeStart, self._randomPlatform) + bs.timer(timeStart, self.startCounter) + + self.spawnAllMap() + self.flashHide() + + # Various high-level game events come through this method. + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = bs.time() + + # Record the player's moment of death. + # assert isinstance(msg.spaz.player + msg.getplayer(Player).death_time = curtime + + # In co-op mode, end the game the instant everyone dies + # (more accurate looking). + # In teams/ffa, allow a one-second fudge-factor so we can + # get more draws if players die basically at the same time. + if isinstance(self.session, bs.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + babase.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = curtime + else: + bs.timer(1.0, self._check_end_game) + else: + # Default handler: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + # In co-op, we go till everyone is dead.. otherwise we go + # until one team remains. + if isinstance(self.session, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def end_game(self) -> None: + cur_time = bs.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + # Mark death-time as now for any still-living players + # and award players points for how long they lasted. + # (these per-player scores are only meaningful in team-games) + for team in self.teams: + for player in team.players: + survived = False + + # Throw an extra fudge factor in so teams that + # didn't die come out ahead of teams that did. + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + # Award a per-player score depending on how many seconds + # they lasted (per-player scores only affect teams mode; + # everywhere else just looks at the per-team score). + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 # A bit extra for survivors. + self.stats.player_scored(player, score, screenmessage=False) + + # Stop updating our time text, and set the final time to match + # exactly when our last guy died. + self._timer.stop(endtime=self._last_player_death_time) + + # Ok now calc game results: set a score for each team and then tell + # the game to end. + results = bs.GameResults() + + # Remember that 'free-for-all' mode is simply a special form + # of 'teams' mode where each player gets their own team, so we can + # just always deal in teams and have all cases covered. + for team in self.teams: + + # Set the team score to the max time survived by any player on + # that team. + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) + + +class MGdefs(): + points = {} + boxes = {} + boxes['area_of_interest_bounds'] = ( + 0.3544110667, 4.493562578, -2.518391331) + (0.0, 0.0, 0.0) + (16.64754831, 8.06138989, 18.5029888) + boxes['map_bounds'] = (0.2608783669, 4.899663734, -3.543675157) + \ + (0.0, 0.0, 0.0) + (29.23565494, 14.19991443, 29.92689344) + + +class MGmap(bs.Map): + defs = MGdefs() + name = 'Sky Tiles' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'achievementOffYouGo' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'bgtex': bs.gettexture('menuBG'), + 'bgmesh': bs.getmesh('thePadBG') + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + gnode = bs.getactivity().globalsnode + gnode.tint = (1.3, 1.2, 1.0) + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + + +bs._map.register_map(MGmap) + + +# ba_meta export babase.Plugin +class byFreaku(babase.Plugin): + def __init__(self): + ## Campaign support ## + babase.app.classic.add_coop_practice_level(bs.Level( + name='Memory Game', displayname='${GAME}', gametype=MGgame, settings={}, preview_texture_name='achievementOffYouGo')) diff --git a/plugins/minigames/meteor_shower.py b/plugins/minigames/meteor_shower.py new file mode 100644 index 000000000..5cb2f8c01 --- /dev/null +++ b/plugins/minigames/meteor_shower.py @@ -0,0 +1,409 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = bs.app.lang.language + +if lang == 'Spanish': + name = 'Lluvia de Meteoritos v2' + bomb_type = 'Tipo de Bomba' + ice = 'hielo' + sticky = 'pegajosa' + impact = 'insta-bomba' + land_mine = 'mina terrestre' + random_bomb = 'aleatoria' + normal_rain = 'Lluvia Normal' + frozen_rain = 'Lluvia Congelada' + sticky_rain = 'Lluvia Pegajosa' + impact_rain = 'Lluvia de Impacto' + mine_rain = 'Lluvia de Minas' + tnt_rain = 'Lluvia de TNT' + random_rain = 'Lluvia Aleatoria' +else: + name = 'Meteor Shower v2' + bomb_type = 'Bomb Type' + ice = 'ice' + sticky = 'sticky' + impact = 'impact' + land_mine = 'land mine' + random_bomb = 'random' + normal_rain = 'Normal Rain' + frozen_rain = 'Frozen Rain' + sticky_rain = 'Sticky Rain' + impact_rain = 'Impact Rain' + mine_rain = 'Mine Rain' + tnt_rain = 'TNT Rain' + random_rain = 'Random Rain' + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: float | None = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class MeteorShowerv2Game(bs.TeamGameActivity[Player, Team]): + """Minigame involving dodging falling bombs.""" + + name = name + description = 'Dodge the falling bombs.' + scoreconfig = bs.ScoreConfig( + label='Survived', scoretype=bs.ScoreType.MILLISECONDS, version='B' + ) + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + + # Don't allow joining after we start + # (would enable leave/rejoin tomfoolery). + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[babase.Setting]: + settings = [ + bs.IntChoiceSetting( + bomb_type, + choices=[ + ('normal', 0), + (ice, 1), + (sticky, 2), + (impact, 3), + (land_mine, 4), + ('tnt', 5), + (random_bomb, 6) + ], + default=0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + # We're currently hard-coded for one map. + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Rampage'] + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return ( + issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession) + or issubclass(sessiontype, bs.CoopSession) + ) + + def __init__(self, settings: dict): + super().__init__(settings) + btype = int(settings[bomb_type]) + if btype == 0: + newbtype = 'normal' + elif btype == 1: + newbtype = 'ice' + elif btype == 2: + newbtype = 'sticky' + elif btype == 3: + newbtype = 'impact' + elif btype == 4: + newbtype = 'land_mine' + elif btype == 5: + newbtype = 'tnt' + else: + newbtype = 'random' + self._bomb_type = newbtype + self._epic_mode = settings.get('Epic Mode', False) + self._last_player_death_time: float | None = None + self._meteor_time = 2.0 + self._timer: OnScreenTimer | None = None + + # Some base class overrides: + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL + ) + if self._epic_mode: + self.slow_motion = True + + def on_begin(self) -> None: + super().on_begin() + + # Drop a wave every few seconds.. and every so often drop the time + # between waves ..lets have things increase faster if we have fewer + # players. + delay = 5.0 if len(self.players) > 2 else 2.5 + if self._epic_mode: + delay *= 0.25 + bs.timer(delay, self._decrement_meteor_time, repeat=True) + + # Kick off the first wave in a few seconds. + delay = 3.0 + if self._epic_mode: + delay *= 0.25 + bs.timer(delay, self._set_meteor_timer) + + self._timer = OnScreenTimer() + self._timer.start() + + # Check for immediate end (if we've only got 1 player, etc). + bs.timer(5.0, self._check_end_game) + + def on_player_leave(self, player: Player) -> None: + # Augment default behavior. + super().on_player_leave(player) + + # A departing player may trigger game-over. + self._check_end_game() + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> bs.Actor: + spaz = self.spawn_player_spaz(player) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player( + enable_punch=False, enable_bomb=False, enable_pickup=False + ) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + # Various high-level game events come through this method. + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = bs.time() + + # Record the player's moment of death. + # assert isinstance(msg.spaz.player + msg.getplayer(Player).death_time = curtime + + # In co-op mode, end the game the instant everyone dies + # (more accurate looking). + # In teams/ffa, allow a one-second fudge-factor so we can + # get more draws if players die basically at the same time. + if isinstance(self.session, bs.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + babase.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = curtime + else: + bs.timer(1.0, self._check_end_game) + + else: + # Default handler: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + # In co-op, we go till everyone is dead.. otherwise we go + # until one team remains. + if isinstance(self.session, bs.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def _set_meteor_timer(self) -> None: + bs.timer( + (1.0 + 0.2 * random.random()) * self._meteor_time, + self._drop_bomb_cluster, + ) + + def _drop_bomb_cluster(self) -> None: + + # Random note: code like this is a handy way to plot out extents + # and debug things. + loc_test = False + if loc_test: + bs.newnode('locator', attrs={'position': (8, 6, -5.5)}) + bs.newnode('locator', attrs={'position': (8, 6, -2.3)}) + bs.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) + bs.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) + + # Drop several bombs in series. + delay = 0.0 + for _i in range(random.randrange(1, 3)): + # Drop them somewhere within our bounds with velocity pointing + # toward the opposite side. + pos = ( + -7.3 + 15.3 * random.random(), + 11, + -5.57 + 2.1 * random.random(), + ) + dropdir = -1.0 if pos[0] > 0 else 1.0 + vel = ( + (-5.0 + random.random() * 30.0) * dropdir, + random.uniform(-3.066, -4.12), + 0, + ) + bs.timer(delay, babase.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + def _drop_bomb( + self, position: Sequence[float], velocity: Sequence[float] + ) -> None: + if self._bomb_type == 'tnt': + bomb_type = random.choice(['tnt', 'tnt', 'tnt', 'tnt', 'impact']) + elif self._bomb_type == 'land_mine': + bomb_type = random.choice([ + 'land_mine', 'land_mine', 'land_mine', 'land_mine', 'impact']) + elif self._bomb_type == 'random': + bomb_type = random.choice([ + 'normal', 'ice', 'sticky', 'impact', 'land_mine', 'tnt']) + else: + bomb_type = self._bomb_type + Bomb(position=position, + velocity=velocity, + bomb_type=bomb_type).autoretain() + + def _decrement_meteor_time(self) -> None: + self._meteor_time = max(0.01, self._meteor_time * 0.9) + + def end_game(self) -> None: + cur_time = bs.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + # Mark death-time as now for any still-living players + # and award players points for how long they lasted. + # (these per-player scores are only meaningful in team-games) + for team in self.teams: + for player in team.players: + survived = False + + # Throw an extra fudge factor in so teams that + # didn't die come out ahead of teams that did. + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + # Award a per-player score depending on how many seconds + # they lasted (per-player scores only affect teams mode; + # everywhere else just looks at the per-team score). + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 # A bit extra for survivors. + self.stats.player_scored(player, score, screenmessage=False) + + # Stop updating our time text, and set the final time to match + # exactly when our last guy died. + self._timer.stop(endtime=self._last_player_death_time) + + # Ok now calc game results: set a score for each team and then tell + # the game to end. + results = bs.GameResults() + + # Remember that 'free-for-all' mode is simply a special form + # of 'teams' mode where each player gets their own team, so we can + # just always deal in teams and have all cases covered. + for team in self.teams: + + # Set the team score to the max time survived by any player on + # that team. + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) + + +# ba_meta export plugin +class MeteorShowerv2Coop(babase.Plugin): + def on_app_running(self) -> None: + babase.app.classic.add_coop_practice_level( + bs._level.Level( + normal_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 0}, + preview_texture_name='rampagePreview', + ) + ) + babase.app.classic.add_coop_practice_level( + bs._level.Level( + frozen_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 1}, + preview_texture_name='rampagePreview', + ) + ) + babase.app.classic.add_coop_practice_level( + bs._level.Level( + sticky_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 2}, + preview_texture_name='rampagePreview', + ) + ) + babase.app.classic.add_coop_practice_level( + bs._level.Level( + impact_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 3}, + preview_texture_name='rampagePreview', + ) + ) + babase.app.classic.add_coop_practice_level( + bs._level.Level( + mine_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 4}, + preview_texture_name='rampagePreview', + ) + ) + babase.app.classic.add_coop_practice_level( + bs._level.Level( + tnt_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 5}, + preview_texture_name='rampagePreview', + ) + ) + babase.app.classic.add_coop_practice_level( + bs._level.Level( + random_rain, + gametype=MeteorShowerv2Game, + settings={bomb_type: 6}, + preview_texture_name='rampagePreview', + ) + ) diff --git a/plugins/minigames/meteor_shower_deluxe.py b/plugins/minigames/meteor_shower_deluxe.py new file mode 100644 index 000000000..296c8dcd3 --- /dev/null +++ b/plugins/minigames/meteor_shower_deluxe.py @@ -0,0 +1,67 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +""" +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. <[1](https://fsf.org/)> + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This license is designed to ensure cooperation with the community in the case of network server software. It is a free, copyleft license for software and other kinds of works. The license guarantees your freedom to share and change all versions of a program, to make sure it remains free software for all its users. + +The license identifier refers to the choice to use code under AGPL-3.0-or-later (i.e., AGPL-3.0 or some later version), as distinguished from use of code under AGPL-3.0-only. The license notice states which of these applies the code in the file. + + +""" +import random +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.game.meteorshower import MeteorShowerGame +from bascenev1lib.actor.bomb import Bomb + + +class NewMeteorShowerGame(MeteorShowerGame): + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps("melee") + + def _drop_bomb_cluster(self) -> None: + # Drop several bombs in series. + delay = 0.0 + bounds = list(self._map.get_def_bound_box("map_bounds")) + for _i in range(random.randrange(1, 3)): + # Drop them somewhere within our bounds with velocity pointing + # toward the opposite side. + pos = ( + random.uniform(bounds[0], bounds[3]), + bounds[4], + random.uniform(bounds[2], bounds[5]), + ) + dropdirx = -1 if pos[0] > 0 else 1 + dropdirz = -1 if pos[2] > 0 else 1 + forcex = ( + bounds[0] - bounds[3] + if bounds[0] - bounds[3] > 0 + else -(bounds[0] - bounds[3]) + ) + forcez = ( + bounds[2] - bounds[5] + if bounds[2] - bounds[5] > 0 + else -(bounds[2] - bounds[5]) + ) + vel = ( + (-5 + random.random() * forcex) * dropdirx, + random.uniform(-3.066, -4.12), + (-5 + random.random() * forcez) * dropdirz, + ) + bs.timer(delay, babase.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + +# ba_meta export plugin +class byEra0S(babase.Plugin): + MeteorShowerGame.get_supported_maps = NewMeteorShowerGame.get_supported_maps + MeteorShowerGame._drop_bomb_cluster = NewMeteorShowerGame._drop_bomb_cluster diff --git a/plugins/minigames/musical_flags.py b/plugins/minigames/musical_flags.py new file mode 100644 index 000000000..f5aa47cd2 --- /dev/null +++ b/plugins/minigames/musical_flags.py @@ -0,0 +1,287 @@ +# Made by MattZ45986 on GitHub +# Ported by your friend: Freaku + + +# Bug Fixes & Improvements as well... + +# Join BCS: +# https://discord.gg/ucyaesh + + +from __future__ import annotations +from typing import TYPE_CHECKING +import _babase +import random +import math +import bascenev1 as bs +from bascenev1lib.actor.flag import Flag, FlagPickedUpMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + + +class Player(bs.Player['Team']): + def __init__(self) -> None: + self.done: bool = False + self.survived: bool = True + + +class Team(bs.Team[Player]): + def __init__(self) -> None: + self.score = 0 + + +# ba_meta require api 9 +# ba_meta export bascenev1.GameActivity +class MFGame(bs.TeamGameActivity[Player, Team]): + name = 'Musical Flags' + description = "Don't be the one stuck without a flag!" + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + 'Max Round Time', + min_value=15, + default=25, + increment=5, + ), + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Enable Running', default=True), + bs.BoolSetting('Enable Punching', default=False), + bs.BoolSetting('Enable Bottom Credit', True) + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Doom Shroom'] + + def __init__(self, settings: dict): + super().__init__(settings) + self.nodes = [] + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self.credit_text = bool(settings['Enable Bottom Credit']) + self.is_punch = bool(settings['Enable Punching']) + self.is_run = bool(settings['Enable Running']) + + self._textRound = bs.newnode('text', + attrs={'text': '', + 'position': (0, -38), + 'scale': 1, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (1.0, 0.0, 1.0), + 'opacity': 1, + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'v_align': 'center'}) + self.round_time = int(settings['Max Round Time']) + self.reset_round_time = int(settings['Max Round Time']) + self.should_die_occur = True + self.round_time_textnode = bs.newnode('text', + attrs={ + 'text': "", 'flatness': 1.0, 'h_align': 'center', 'h_attach': 'center', 'v_attach': 'top', 'v_align': 'center', 'position': (0, -15), 'scale': 0.9, 'color': (1, 0.7, 0.9)}) + + self.slow_motion = self._epic_mode + # A cool music, matching our gamemode theme + self.default_music = bs.MusicType.FLAG_CATCHER + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Catch Flag for yourself' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'Catch Flag for yourself' + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + bs.broadcastmessage( + bs.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), transient=True) + player.survived = False + return + self.spawn_player(player) + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + # A departing player may trigger game-over. + bs.timer(0, self.checkEnd) + + def on_begin(self) -> None: + super().on_begin() + self.roundNum = 0 + self.numPickedUp = 0 + self.nodes = [] + self.flags = [] + self.spawned = [] + if self.credit_text: + t = bs.newnode('text', + attrs={'text': "Ported by Freaku\nMade by MattZ45986", # Disable 'Enable Bottom Credits' when making playlist, No need to edit this lovely... + 'scale': 0.7, + 'position': (0, 0), + 'shadow': 0.5, + 'flatness': 1.2, + 'color': (1, 1, 1), + 'h_align': 'center', + 'v_attach': 'bottom'}) + self.makeRound() + self._textRound.text = 'Round ' + str(self.roundNum) + bs.timer(3, self.checkEnd) + self.keepcalling = bs.timer(1, self._timeround, True) + + def _timeround(self): + if self.round_time == 0 and self.should_die_occur: + self.should_die_occur = False + self.round_time_textnode.opacity = 0 + bs.broadcastmessage('Proceeding Round...') + for player in self.spawned: + if not player.done: + try: + player.survived = False + player.actor.handlemessage(bs.StandMessage((0, 3, -2))) + bs.timer(0.5, bs.Call(player.actor.handlemessage, bs.FreezeMessage())) + bs.timer(1.5, bs.Call(player.actor.handlemessage, bs.FreezeMessage())) + bs.timer(2.5, bs.Call(player.actor.handlemessage, bs.FreezeMessage())) + bs.timer(3, bs.Call(player.actor.handlemessage, bs.ShouldShatterMessage())) + except: + pass + bs.timer(3.5, self.killRound) + bs.timer(3.55, self.makeRound) + self.round_time_textnode.opacity = 0 + self.round_time = self.reset_round_time + else: + self.round_time_textnode.text = "Time: " + str(self.round_time) + self.round_time -= 1 + + def makeRound(self): + for player in self.players: + if player.survived: + player.team.score += 1 + self.roundNum += 1 + self._textRound.text = 'Round ' + str(self.roundNum) + self.flags = [] + self.spawned = [] + self.should_die_occur = True + self.round_time = self.reset_round_time + self.round_time_textnode.opacity = 1 + angle = random.randint(0, 359) + c = 0 + for player in self.players: + if player.survived: + c += 1 + spacing = 10 + for player in self.players: + player.done = False + if player.survived: + if not player.is_alive(): + self.spawn_player(player, (.5, 5, -4)) + self.spawned.append(player) + try: + spacing = 360 // (c) + except: + self.checkEnd() + colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (1, 0, 1), (0, 1, 1), (0, 0, 0), + (0.5, 0.8, 0), (0, 0.8, 0.5), (0.8, 0.25, 0.7), (0, 0.27, 0.55), (2, 2, 0.6), (0.4, 3, 0.85)] + + # Add support for more than 13 players + if c > 12: + for i in range(c-12): + colors.append((random.uniform(0.1, 1), random.uniform( + 0.1, 1), random.uniform(0.1, 1))) + + # Smart Mathematics: + # All Flags spawn same distance from the players + for i in range(c-1): + angle += spacing + angle %= 360 + x = 6 * math.sin(math.degrees(angle)) + z = 6 * math.cos(math.degrees(angle)) + flag = Flag(position=(x+.5, 5, z-4), color=colors[i]).autoretain() + self.flags.append(flag) + + def killRound(self): + self.numPickedUp = 0 + for player in self.players: + if player.is_alive(): + player.actor.handlemessage(bs.DieMessage()) + for flag in self.flags: + flag.node.delete() + for light in self.nodes: + light.delete() + + def spawn_player(self, player: Player, pos: tuple = (0, 0, 0)) -> bs.Actor: + spaz = self.spawn_player_spaz(player) + if pos == (0, 0, 0): + pos = (-.5+random.random()*2, 3+random.random()*2, -5+random.random()*2) + spaz.connect_controls_to_player(enable_punch=self.is_punch, + enable_bomb=False, enable_run=self.is_run) + spaz.handlemessage(bs.StandMessage(pos)) + return spaz + + def check_respawn(self, player): + if not player.done and player.survived: + self.respawn_player(player, 2.5) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) + player = msg.getplayer(Player) + bs.timer(0.1, bs.Call(self.check_respawn, player)) + bs.timer(0.5, self.checkEnd) + elif isinstance(msg, FlagPickedUpMessage): + self.numPickedUp += 1 + msg.node.getdelegate(PlayerSpaz, True).getplayer(Player, True).done = True + l = bs.newnode('light', + owner=None, + attrs={'color': msg.node.color, + 'position': (msg.node.position_center), + 'intensity': 1}) + self.nodes.append(l) + msg.flag.handlemessage(bs.DieMessage()) + msg.node.handlemessage(bs.DieMessage()) + msg.node.delete() + if self.numPickedUp == len(self.flags): + self.round_time_textnode.opacity = 0 + self.round_time = self.reset_round_time + for player in self.spawned: + if not player.done: + try: + player.survived = False + bs.broadcastmessage("No Flag? "+player.getname()) + player.actor.handlemessage(bs.StandMessage((0, 3, -2))) + bs.timer(0.5, bs.Call(player.actor.handlemessage, bs.FreezeMessage())) + bs.timer(3, bs.Call(player.actor.handlemessage, bs.ShouldShatterMessage())) + except: + pass + bs.timer(3.5, self.killRound) + bs.timer(3.55, self.makeRound) + else: + return super().handlemessage(msg) + return None + + def checkEnd(self): + i = 0 + for player in self.players: + if player.survived: + i += 1 + if i <= 1: + for player in self.players: + if player.survived: + player.team.score += 10 + bs.timer(2.5, self.end_game) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/plugins/minigames/ofuuu_attack.py b/plugins/minigames/ofuuu_attack.py new file mode 100644 index 000000000..1e5b6d479 --- /dev/null +++ b/plugins/minigames/ofuuu_attack.py @@ -0,0 +1,382 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor.bomb import BombFactory, Bomb +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List, Dict, Type, Type + + +class _GotTouched(): + pass + + +class UFO(bs.Actor): + + def __init__(self, pos: float = (0, 0, 0)): + super().__init__() + shared = SharedObjects.get() + self.r: Optional[int] = 0 + self.dis: Optional[List] = [] + self.target: float = (0.0, 0.0, 0.0) + self.regs: List[bs.NodeActor] = [] + self.node = bs.newnode('prop', + delegate=self, + attrs={'body': 'landMine', + 'position': pos, + 'mesh': bs.getmesh('landMine'), + 'mesh_scale': 1.5, + 'body_scale': 0.01, + 'shadow_size': 0.000001, + 'gravity_scale': 0.0, + 'color_texture': bs.gettexture("achievementCrossHair"), + 'materials': [shared.object_material]}) + self.ufo_collide = None + + def create_target(self): + if not self.node.exists(): + return + self.dis = [] + shared = SharedObjects.get() + try: + def pass_(): + self.regs.clear() + bs.timer(3875*0.001, self.move) + try: + bs.timer(3277*0.001, lambda: Bomb(velocity=(0, 0, 0), position=( + self.target[0], self.node.position[1]-0.43999, self.target[2]), bomb_type='impact').autoretain().arm()) + except: + pass + key = bs.Material() + key.add_actions( + conditions=('they_have_material', shared.object_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', pass_()), + )) + except: + pass + self.regs.append(bs.NodeActor(bs.newnode('region', + attrs={ + 'position': self.target, + 'scale': (0.04, 22, 0.04), + 'type': 'sphere', + 'materials': [key]}))) + + def move(self): + if not self.node.exists(): + return + try: + self.create_target() + for j in bs.getnodes(): + n = j.getdelegate(object) + if j.getnodetype() == 'prop' and isinstance(n, TileFloor): + if n.node.exists(): + self.dis.append(n.node) + self.r = random.randint(0, len(self.dis)-1) + self.target = (self.dis[self.r].position[0], + self.node.position[1], self.dis[self.r].position[2]) + bs.animate_array(self.node, 'position', 3, { + 0: self.node.position, + 3.0: self.target}) + except: + pass + + def handlemessage(self, msg): + + if isinstance(msg, bs.DieMessage): + self.node.delete() + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + else: + super().handlemessage(msg) + + +class TileFloor(bs.Actor): + def __init__(self, + pos: float = (0, 0, 0)): + super().__init__() + get_mat = SharedObjects.get() + self.pos = pos + self.scale = 1.5 + self.mat, self.mat2, self.test = bs.Material(), bs.Material(), bs.Material() + self.mat.add_actions(conditions=('we_are_older_than', 1), + actions=(('modify_part_collision', 'collide', False))) + self.mat2.add_actions(conditions=('we_are_older_than', 1), + actions=(('modify_part_collision', 'collide', True))) + self.test.add_actions( + conditions=('they_have_material', BombFactory.get().bomb_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('message', 'our_node', 'at_connect', _GotTouched()))) + self.node = bs.newnode('prop', + delegate=self, + attrs={'body': 'puck', + 'position': self.pos, + 'mesh': bs.getmesh('buttonSquareOpaque'), + 'mesh_scale': self.scale*1.16, + 'body_scale': self.scale, + 'shadow_size': 0.0002, + 'gravity_scale': 0.0, + 'color_texture': bs.gettexture("tnt"), + 'is_area_of_interest': True, + 'materials': [self.mat, self.test]}) + self.node_support = bs.newnode('region', + attrs={ + 'position': self.pos, + 'scale': (self.scale*0.8918, 0.1, self.scale*0.8918), + 'type': 'box', + 'materials': [get_mat.footing_material, self.mat2] + }) + + def handlemessage(self, msg): + if isinstance(msg, bs.DieMessage): + self.node.delete() + self.node_support.delete() + elif isinstance(msg, _GotTouched): + def do(): self.handlemessage(bs.DieMessage()) + bs.timer(0.1, do) + else: + super().handlemessage(msg) + + +class defs(): + points = boxes = {} + boxes['area_of_interest_bounds'] = (-1.3440, 1.185751251, 3.7326226188) + ( + 0.0, 0.0, 0.0) + (29.8180273, 15.57249038, 22.93859993) + boxes['map_bounds'] = (0.0, 2.585751251, 0.4326226188) + (0.0, 0.0, + 0.0) + (29.09506485, 15.81173179, 33.76723155) + + +class DummyMapForGame(bs.Map): + defs, name = defs(), 'Tile Lands' + + @classmethod + def get_play_types(cls) -> List[str]: + return [] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'achievementCrossHair' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = {'bg_1': bs.gettexture('rampageBGColor'), 'bg_2': bs.gettexture( + 'rampageBGColor2'), 'bg_mesh_1': bs.getmesh('rampageBG'), 'bg_mesh_2': bs.getmesh('rampageBG2'), } + return data + + def __init__(self) -> None: + super().__init__() + self.bg1 = bs.newnode('terrain', attrs={ + 'mesh': self.preloaddata['bg_mesh_1'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bg_2']}) + self.bg2 = bs.newnode('terrain', attrs={ + 'mesh': self.preloaddata['bg_mesh_2'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bg_2']}) + a = bs.getactivity().globalsnode + a.tint, a.ambient_color, a.vignette_outer, a.vignette_inner = ( + 1.2, 1.1, 0.97), (1.3, 1.2, 1.03), (0.62, 0.64, 0.69), (0.97, 0.95, 0.93) + + +class DummyMapForGame2(bs.Map): + defs, name = defs(), 'Tile Lands Night' + + @classmethod + def get_play_types(cls) -> List[str]: + return [] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'achievementCrossHair' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = {'bg_1': bs.gettexture('menuBG'), 'bg_2': bs.gettexture( + 'menuBG'), 'bg_mesh_1': bs.getmesh('thePadBG'), 'bg_mesh_2': bs.getmesh('thePadBG'), } + return data + + def __init__(self) -> None: + super().__init__() + self.bg1 = bs.newnode('terrain', attrs={ + 'mesh': self.preloaddata['bg_mesh_1'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bg_2']}) + self.bg2 = bs.newnode('terrain', attrs={ + 'mesh': self.preloaddata['bg_mesh_2'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bg_2']}) + a = bs.getactivity().globalsnode + a.tint, a.ambient_color, a.vignette_outer, a.vignette_inner = ( + 0.5, 0.7, 1.27), (2.5, 2.5, 2.5), (0.62, 0.64, 0.69), (0.97, 0.95, 0.93) + + +bs._map.register_map(DummyMapForGame) +bs._map.register_map(DummyMapForGame2) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class UFOAttackGame(bs.TeamGameActivity[Player, Team]): + + name = 'UFO Attack' + description = 'Dodge the falling bombs.' + available_settings = [ + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Enable Run', default=True), + bs.BoolSetting('Enable Jump', default=True), + bs.BoolSetting('Display Map Area Dimension', default=False), + bs.IntSetting('No. of Rows' + u' →', max_value=13, min_value=1, default=8, increment=1), + bs.IntSetting('No. of Columns' + u' ↓', max_value=12, min_value=1, default=6, increment=1) + ] + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, + version='B') + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Tile Lands', 'Tile Lands Night'] + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + + self.col = int(settings['No. of Columns' + u' ↓']) + self.row = int(settings['No. of Rows' + u' →']) + self.bool1 = bool(settings['Enable Run']) + self.bool2 = bool(settings['Enable Jump']) + self._epic_mode = settings.get('Epic Mode', False) + self._last_player_death_time: Optional[float] = None + self._timer: Optional[OnScreenTimer] = None + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + if bool(settings["Display Map Area Dimension"]): + self.game_name = "UFO Attack " + "(" + str(self.col) + "x" + str(self.row) + ")" + else: + self.game_name = "UFO Attack" + if self._epic_mode: + self.slow_motion = True + + def get_instance_display_string(self) -> babase.Lstr: + return self.game_name + + def on_begin(self) -> None: + super().on_begin() + self._timer = OnScreenTimer() + self._timer.start() + # bs.timer(5.0, self._check_end_game) + for r in range(self.col): + for j in range(self.row): + tile = TileFloor(pos=(-6.204283+(j*1.399), 3.425666, + -1.3538+(r*1.399))).autoretain() + self.ufo = UFO(pos=(-5.00410667, 6.616383286, -2.503472)).autoretain() + bs.timer(7000*0.001, lambda: self.ufo.move()) + for t in self.players: + self.spawn_player(t) + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + assert self._timer is not None + player.death_time = self._timer.getstarttime() + return + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + self._check_end_game() + + def spawn_player(self, player: Player) -> bs.Actor: + dis = [] + for a in bs.getnodes(): + g = a.getdelegate(object) + if a.getnodetype() == 'prop' and isinstance(g, TileFloor): + dis.append(g.node) + r = random.randint(0, len(dis)-1) + spaz = self.spawn_player_spaz(player, position=( + dis[r].position[0], dis[r].position[1]+1.005958, dis[r].position[2])) + spaz.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_run=self.bool1, + enable_jump=self.bool2, + enable_pickup=False) + spaz.play_big_death_sound = True + return spaz + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) + + curtime = bs.time() + msg.getplayer(Player).death_time = curtime + bs.timer(1.0, self._check_end_game) + + else: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + if living_team_count <= 1: + self.end_game() + + def end_game(self) -> None: + self.ufo.handlemessage(bs.DieMessage()) + cur_time = bs.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + for team in self.teams: + for player in team.players: + survived = False + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 2 + self.stats.player_scored(player, score, screenmessage=False) + self._timer.stop(endtime=self._last_player_death_time) + results = bs.GameResults() + for team in self.teams: + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, + player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(longest_life)) + + self.end(results=results) diff --git a/plugins/minigames/onslaught_football.py b/plugins/minigames/onslaught_football.py new file mode 100644 index 000000000..b585581a7 --- /dev/null +++ b/plugins/minigames/onslaught_football.py @@ -0,0 +1,1027 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations +from asyncio import base_subprocess + +import math +import random +from enum import Enum, unique +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.popuptext import PopupText +from bascenev1lib.actor.bomb import TNTSpawner +from bascenev1lib.actor.playerspaz import PlayerSpazHurtMessage +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.controlsguide import ControlsGuide +from bascenev1lib.actor.powerupbox import PowerupBox, PowerupBoxFactory +from bascenev1lib.actor.spazbot import ( + SpazBotDiedMessage, + SpazBotSet, + ChargerBot, + StickyBot, + BomberBot, + BomberBotLite, + BrawlerBot, + BrawlerBotLite, + TriggerBot, + BomberBotStaticLite, + TriggerBotStatic, + BomberBotProStatic, + TriggerBotPro, + ExplodeyBot, + BrawlerBotProShielded, + ChargerBotProShielded, + BomberBotPro, + TriggerBotProShielded, + BrawlerBotPro, + BomberBotProShielded, +) + +if TYPE_CHECKING: + from typing import Any, Sequence + from bascenev1lib.actor.spazbot import SpazBot + + +@dataclass +class Wave: + """A wave of enemies.""" + + entries: list[Spawn | Spacing | Delay | None] + base_angle: float = 0.0 + + +@dataclass +class Spawn: + """A bot spawn event in a wave.""" + + bottype: type[SpazBot] | str + point: Point | None = None + spacing: float = 5.0 + + +@dataclass +class Spacing: + """Empty space in a wave.""" + + spacing: float = 5.0 + + +@dataclass +class Delay: + """A delay between events in a wave.""" + + duration: float + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.has_been_hurt = False + self.respawn_wave = 0 + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +class OnslaughtFootballGame(bs.CoopGameActivity[Player, Team]): + """Co-op game where players try to survive attacking waves of enemies.""" + + name = 'Onslaught' + description = 'Defeat all enemies.' + + tips: list[str | babase.GameTip] = [ + 'Hold any button to run.' + ' (Trigger buttons work well if you have them)', + 'Try tricking enemies into killing eachother or running off cliffs.', + 'Try \'Cooking off\' bombs for a second or two before throwing them.', + 'It\'s easier to win with a friend or two helping.', + 'If you stay in one place, you\'re toast. Run and dodge to survive..', + 'Practice using your momentum to throw bombs more accurately.', + 'Your punches do much more damage if you are running or spinning.', + ] + + # Show messages when players die since it matters here. + announce_player_deaths = True + + def __init__(self, settings: dict): + super().__init__(settings) + self._new_wave_sound = bs.getsound('scoreHit01') + self._winsound = bs.getsound('score') + self._cashregistersound = bs.getsound('cashRegister') + self._a_player_has_been_hurt = False + self._player_has_dropped_bomb = False + self._spawn_center = (0, 0.2, 0) + self._tntspawnpos = (0, 0.95, -0.77) + self._powerup_center = (0, 1.5, 0) + self._powerup_spread = (6.0, 4.0) + self._scoreboard: Scoreboard | None = None + self._game_over = False + self._wavenum = 0 + self._can_end_wave = True + self._score = 0 + self._time_bonus = 0 + self._spawn_info_text: bs.NodeActor | None = None + self._dingsound = bs.getsound('dingSmall') + self._dingsoundhigh = bs.getsound('dingSmallHigh') + self._have_tnt = False + self._excluded_powerups: list[str] | None = None + self._waves: list[Wave] = [] + self._tntspawner: TNTSpawner | None = None + self._bots: SpazBotSet | None = None + self._powerup_drop_timer: bs.Timer | None = None + self._time_bonus_timer: bs.Timer | None = None + self._time_bonus_text: bs.NodeActor | None = None + self._flawless_bonus: int | None = None + self._wave_text: bs.NodeActor | None = None + self._wave_update_timer: bs.Timer | None = None + self._throw_off_kills = 0 + self._land_mine_kills = 0 + self._tnt_kills = 0 + + self._epic_mode = bool(settings['Epic Mode']) + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.ONSLAUGHT + ) + + def on_transition_in(self) -> None: + super().on_transition_in() + self._spawn_info_text = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'position': (15, -130), + 'h_attach': 'left', + 'v_attach': 'top', + 'scale': 0.55, + 'color': (0.3, 0.8, 0.3, 1.0), + 'text': '', + }, + ) + ) + self._scoreboard = Scoreboard( + label=babase.Lstr(resource='scoreText'), score_split=0.5 + ) + + def on_begin(self) -> None: + super().on_begin() + self._have_tnt = True + self._excluded_powerups = [] + self._waves = [] + bs.timer(4.0, self._start_powerup_drops) + + # Our TNT spawner (if applicable). + if self._have_tnt: + self._tntspawner = TNTSpawner(position=self._tntspawnpos) + + self.setup_low_life_warning_sound() + self._update_scores() + self._bots = SpazBotSet() + bs.timer(4.0, self._start_updating_waves) + self._next_ffa_start_index = random.randrange( + len(self.map.get_def_points('ffa_spawn')) + ) + + def _get_dist_grp_totals(self, grps: list[Any]) -> tuple[int, int]: + totalpts = 0 + totaldudes = 0 + for grp in grps: + for grpentry in grp: + dudes = grpentry[1] + totalpts += grpentry[0] * dudes + totaldudes += dudes + return totalpts, totaldudes + + def _get_distribution( + self, + target_points: int, + min_dudes: int, + max_dudes: int, + group_count: int, + max_level: int, + ) -> list[list[tuple[int, int]]]: + """Calculate a distribution of bad guys given some params.""" + max_iterations = 10 + max_dudes * 2 + + groups: list[list[tuple[int, int]]] = [] + for _g in range(group_count): + groups.append([]) + types = [1] + if max_level > 1: + types.append(2) + if max_level > 2: + types.append(3) + if max_level > 3: + types.append(4) + for iteration in range(max_iterations): + diff = self._add_dist_entry_if_possible( + groups, max_dudes, target_points, types + ) + + total_points, total_dudes = self._get_dist_grp_totals(groups) + full = total_points >= target_points + + if full: + # Every so often, delete a random entry just to + # shake up our distribution. + if random.random() < 0.2 and iteration != max_iterations - 1: + self._delete_random_dist_entry(groups) + + # If we don't have enough dudes, kill the group with + # the biggest point value. + elif ( + total_dudes < min_dudes and iteration != max_iterations - 1 + ): + self._delete_biggest_dist_entry(groups) + + # If we've got too many dudes, kill the group with the + # smallest point value. + elif ( + total_dudes > max_dudes and iteration != max_iterations - 1 + ): + self._delete_smallest_dist_entry(groups) + + # Close enough.. we're done. + else: + if diff == 0: + break + + return groups + + def _add_dist_entry_if_possible( + self, + groups: list[list[tuple[int, int]]], + max_dudes: int, + target_points: int, + types: list[int], + ) -> int: + # See how much we're off our target by. + total_points, total_dudes = self._get_dist_grp_totals(groups) + diff = target_points - total_points + dudes_diff = max_dudes - total_dudes + + # Add an entry if one will fit. + value = types[random.randrange(len(types))] + group = groups[random.randrange(len(groups))] + if not group: + max_count = random.randint(1, 6) + else: + max_count = 2 * random.randint(1, 3) + max_count = min(max_count, dudes_diff) + count = min(max_count, diff // value) + if count > 0: + group.append((value, count)) + total_points += value * count + total_dudes += count + diff = target_points - total_points + return diff + + def _delete_smallest_dist_entry( + self, groups: list[list[tuple[int, int]]] + ) -> None: + smallest_value = 9999 + smallest_entry = None + smallest_entry_group = None + for group in groups: + for entry in group: + if entry[0] < smallest_value or smallest_entry is None: + smallest_value = entry[0] + smallest_entry = entry + smallest_entry_group = group + assert smallest_entry is not None + assert smallest_entry_group is not None + smallest_entry_group.remove(smallest_entry) + + def _delete_biggest_dist_entry( + self, groups: list[list[tuple[int, int]]] + ) -> None: + biggest_value = 9999 + biggest_entry = None + biggest_entry_group = None + for group in groups: + for entry in group: + if entry[0] > biggest_value or biggest_entry is None: + biggest_value = entry[0] + biggest_entry = entry + biggest_entry_group = group + if biggest_entry is not None: + assert biggest_entry_group is not None + biggest_entry_group.remove(biggest_entry) + + def _delete_random_dist_entry( + self, groups: list[list[tuple[int, int]]] + ) -> None: + entry_count = 0 + for group in groups: + for _ in group: + entry_count += 1 + if entry_count > 1: + del_entry = random.randrange(entry_count) + entry_count = 0 + for group in groups: + for entry in group: + if entry_count == del_entry: + group.remove(entry) + break + entry_count += 1 + + def spawn_player(self, player: Player) -> bs.Actor: + + # We keep track of who got hurt each wave for score purposes. + player.has_been_hurt = False + pos = ( + self._spawn_center[0] + random.uniform(-1.5, 1.5), + self._spawn_center[1], + self._spawn_center[2] + random.uniform(-1.5, 1.5), + ) + spaz = self.spawn_player_spaz(player, position=pos) + spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) + return spaz + + def _handle_player_dropped_bomb( + self, player: bs.Actor, bomb: bs.Actor + ) -> None: + del player, bomb # Unused. + self._player_has_dropped_bomb = True + + def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: + poweruptype = PowerupBoxFactory.get().get_random_powerup_type( + forcetype=poweruptype, excludetypes=self._excluded_powerups + ) + PowerupBox( + position=self.map.powerup_spawn_points[index], + poweruptype=poweruptype, + ).autoretain() + + def _start_powerup_drops(self) -> None: + self._powerup_drop_timer = bs.Timer( + 3.0, bs.WeakCall(self._drop_powerups), repeat=True + ) + + def _drop_powerups( + self, standard_points: bool = False, poweruptype: str | None = None + ) -> None: + """Generic powerup drop.""" + if standard_points: + points = self.map.powerup_spawn_points + for i in range(len(points)): + bs.timer( + 1.0 + i * 0.5, + bs.WeakCall( + self._drop_powerup, i, poweruptype if i == 0 else None + ), + ) + else: + point = ( + self._powerup_center[0] + + random.uniform( + -1.0 * self._powerup_spread[0], + 1.0 * self._powerup_spread[0], + ), + self._powerup_center[1], + self._powerup_center[2] + + random.uniform( + -self._powerup_spread[1], self._powerup_spread[1] + ), + ) + + # Drop one random one somewhere. + PowerupBox( + position=point, + poweruptype=PowerupBoxFactory.get().get_random_powerup_type( + excludetypes=self._excluded_powerups + ), + ).autoretain() + + def do_end(self, outcome: str, delay: float = 0.0) -> None: + """End the game with the specified outcome.""" + if outcome == 'defeat': + self.fade_to_red() + score: int | None + if self._wavenum >= 2: + score = self._score + fail_message = None + else: + score = None + fail_message = babase.Lstr(resource='reachWave2Text') + self.end( + { + 'outcome': outcome, + 'score': score, + 'fail_message': fail_message, + 'playerinfos': self.initialplayerinfos, + }, + delay=delay, + ) + + def _update_waves(self) -> None: + + # If we have no living bots, go to the next wave. + assert self._bots is not None + if ( + self._can_end_wave + and not self._bots.have_living_bots() + and not self._game_over + ): + self._can_end_wave = False + self._time_bonus_timer = None + self._time_bonus_text = None + base_delay = 0.0 + + # Reward time bonus. + if self._time_bonus > 0: + bs.timer(0, babase.Call(self._cashregistersound.play)) + bs.timer( + base_delay, + bs.WeakCall(self._award_time_bonus, self._time_bonus), + ) + base_delay += 1.0 + + # Reward flawless bonus. + if self._wavenum > 0: + have_flawless = False + for player in self.players: + if player.is_alive() and not player.has_been_hurt: + have_flawless = True + bs.timer( + base_delay, + bs.WeakCall(self._award_flawless_bonus, player), + ) + player.has_been_hurt = False # reset + if have_flawless: + base_delay += 1.0 + + self._wavenum += 1 + + # Short celebration after waves. + if self._wavenum > 1: + self.celebrate(0.5) + bs.timer(base_delay, bs.WeakCall(self._start_next_wave)) + + def _award_completion_bonus(self) -> None: + self._cashregistersound.play() + for player in self.players: + try: + if player.is_alive(): + assert self.initialplayerinfos is not None + self.stats.player_scored( + player, + int(100 / len(self.initialplayerinfos)), + scale=1.4, + color=(0.6, 0.6, 1.0, 1.0), + title=babase.Lstr(resource='completionBonusText'), + screenmessage=False, + ) + except Exception: + babase.print_exception() + + def _award_time_bonus(self, bonus: int) -> None: + self._cashregistersound.play() + PopupText( + babase.Lstr( + value='+${A} ${B}', + subs=[ + ('${A}', str(bonus)), + ('${B}', babase.Lstr(resource='timeBonusText')), + ], + ), + color=(1, 1, 0.5, 1), + scale=1.0, + position=(0, 3, -1), + ).autoretain() + self._score += self._time_bonus + self._update_scores() + + def _award_flawless_bonus(self, player: Player) -> None: + self._cashregistersound.play() + try: + if player.is_alive(): + assert self._flawless_bonus is not None + self.stats.player_scored( + player, + self._flawless_bonus, + scale=1.2, + color=(0.6, 1.0, 0.6, 1.0), + title=babase.Lstr(resource='flawlessWaveText'), + screenmessage=False, + ) + except Exception: + babase.print_exception() + + def _start_time_bonus_timer(self) -> None: + self._time_bonus_timer = bs.Timer( + 1.0, bs.WeakCall(self._update_time_bonus), repeat=True + ) + + def _update_player_spawn_info(self) -> None: + + # If we have no living players lets just blank this. + assert self._spawn_info_text is not None + assert self._spawn_info_text.node + if not any(player.is_alive() for player in self.teams[0].players): + self._spawn_info_text.node.text = '' + else: + text: str | babase.Lstr = '' + for player in self.players: + if not player.is_alive(): + rtxt = babase.Lstr( + resource='onslaughtRespawnText', + subs=[ + ('${PLAYER}', player.getname()), + ('${WAVE}', str(player.respawn_wave)), + ], + ) + text = babase.Lstr( + value='${A}${B}\n', + subs=[ + ('${A}', text), + ('${B}', rtxt), + ], + ) + self._spawn_info_text.node.text = text + + def _respawn_players_for_wave(self) -> None: + # Respawn applicable players. + if self._wavenum > 1 and not self.is_waiting_for_continue(): + for player in self.players: + if ( + not player.is_alive() + and player.respawn_wave == self._wavenum + ): + self.spawn_player(player) + self._update_player_spawn_info() + + def _setup_wave_spawns(self, wave: Wave) -> None: + tval = 0.0 + dtime = 0.2 + if self._wavenum == 1: + spawn_time = 3.973 + tval += 0.5 + else: + spawn_time = 2.648 + + bot_angle = wave.base_angle + self._time_bonus = 0 + self._flawless_bonus = 0 + for info in wave.entries: + if info is None: + continue + if isinstance(info, Delay): + spawn_time += info.duration + continue + if isinstance(info, Spacing): + bot_angle += info.spacing + continue + bot_type_2 = info.bottype + if bot_type_2 is not None: + assert not isinstance(bot_type_2, str) + self._time_bonus += bot_type_2.points_mult * 20 + self._flawless_bonus += bot_type_2.points_mult * 5 + + if self.map.name == 'Doom Shroom': + tval += dtime + spacing = info.spacing + bot_angle += spacing * 0.5 + if bot_type_2 is not None: + tcall = bs.WeakCall( + self.add_bot_at_angle, bot_angle, bot_type_2, spawn_time + ) + bs.timer(tval, tcall) + tval += dtime + bot_angle += spacing * 0.5 + else: + assert bot_type_2 is not None + spcall = bs.WeakCall( + self.add_bot_at_point, bot_type_2, spawn_time + ) + bs.timer(tval, spcall) + + # We can end the wave after all the spawning happens. + bs.timer( + tval + spawn_time - dtime + 0.01, + bs.WeakCall(self._set_can_end_wave), + ) + + def _start_next_wave(self) -> None: + + # This can happen if we beat a wave as we die. + # We don't wanna respawn players and whatnot if this happens. + if self._game_over: + return + + self._respawn_players_for_wave() + wave = self._generate_random_wave() + self._setup_wave_spawns(wave) + self._update_wave_ui_and_bonuses() + bs.timer(0.4, babase.Call(self._new_wave_sound.play)) + + def _update_wave_ui_and_bonuses(self) -> None: + self.show_zoom_message( + babase.Lstr( + value='${A} ${B}', + subs=[ + ('${A}', babase.Lstr(resource='waveText')), + ('${B}', str(self._wavenum)), + ], + ), + scale=1.0, + duration=1.0, + trail=True, + ) + + # Reset our time bonus. + tbtcolor = (1, 1, 0, 1) + tbttxt = babase.Lstr( + value='${A}: ${B}', + subs=[ + ('${A}', babase.Lstr(resource='timeBonusText')), + ('${B}', str(self._time_bonus)), + ], + ) + self._time_bonus_text = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'vr_depth': -30, + 'color': tbtcolor, + 'shadow': 1.0, + 'flatness': 1.0, + 'position': (0, -60), + 'scale': 0.8, + 'text': tbttxt, + }, + ) + ) + + bs.timer(5.0, bs.WeakCall(self._start_time_bonus_timer)) + wtcolor = (1, 1, 1, 1) + wttxt = babase.Lstr( + value='${A} ${B}', + subs=[ + ('${A}', babase.Lstr(resource='waveText')), + ('${B}', str(self._wavenum) + ('')), + ], + ) + self._wave_text = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'vr_depth': -10, + 'color': wtcolor, + 'shadow': 1.0, + 'flatness': 1.0, + 'position': (0, -40), + 'scale': 1.3, + 'text': wttxt, + }, + ) + ) + + def _bot_levels_for_wave(self) -> list[list[type[SpazBot]]]: + level = self._wavenum + bot_types = [ + BomberBot, + BrawlerBot, + TriggerBot, + ChargerBot, + BomberBotPro, + BrawlerBotPro, + TriggerBotPro, + BomberBotProShielded, + ExplodeyBot, + ChargerBotProShielded, + StickyBot, + BrawlerBotProShielded, + TriggerBotProShielded, + ] + if level > 5: + bot_types += [ + ExplodeyBot, + TriggerBotProShielded, + BrawlerBotProShielded, + ChargerBotProShielded, + ] + if level > 7: + bot_types += [ + ExplodeyBot, + TriggerBotProShielded, + BrawlerBotProShielded, + ChargerBotProShielded, + ] + if level > 10: + bot_types += [ + TriggerBotProShielded, + TriggerBotProShielded, + TriggerBotProShielded, + TriggerBotProShielded, + ] + if level > 13: + bot_types += [ + TriggerBotProShielded, + TriggerBotProShielded, + TriggerBotProShielded, + TriggerBotProShielded, + ] + bot_levels = [ + [b for b in bot_types if b.points_mult == 1], + [b for b in bot_types if b.points_mult == 2], + [b for b in bot_types if b.points_mult == 3], + [b for b in bot_types if b.points_mult == 4], + ] + + # Make sure all lists have something in them + if not all(bot_levels): + raise RuntimeError('Got empty bot level') + return bot_levels + + def _add_entries_for_distribution_group( + self, + group: list[tuple[int, int]], + bot_levels: list[list[type[SpazBot]]], + all_entries: list[Spawn | Spacing | Delay | None], + ) -> None: + entries: list[Spawn | Spacing | Delay | None] = [] + for entry in group: + bot_level = bot_levels[entry[0] - 1] + bot_type = bot_level[random.randrange(len(bot_level))] + rval = random.random() + if rval < 0.5: + spacing = 10.0 + elif rval < 0.9: + spacing = 20.0 + else: + spacing = 40.0 + split = random.random() > 0.3 + for i in range(entry[1]): + if split and i % 2 == 0: + entries.insert(0, Spawn(bot_type, spacing=spacing)) + else: + entries.append(Spawn(bot_type, spacing=spacing)) + if entries: + all_entries += entries + all_entries.append(Spacing(40.0 if random.random() < 0.5 else 80.0)) + + def _generate_random_wave(self) -> Wave: + level = self._wavenum + bot_levels = self._bot_levels_for_wave() + + target_points = level * 3 - 2 + min_dudes = min(1 + level // 3, 10) + max_dudes = min(10, level + 1) + max_level = ( + 4 if level > 6 else (3 if level > 3 else (2 if level > 2 else 1)) + ) + group_count = 3 + distribution = self._get_distribution( + target_points, min_dudes, max_dudes, group_count, max_level + ) + all_entries: list[Spawn | Spacing | Delay | None] = [] + for group in distribution: + self._add_entries_for_distribution_group( + group, bot_levels, all_entries + ) + angle_rand = random.random() + if angle_rand > 0.75: + base_angle = 130.0 + elif angle_rand > 0.5: + base_angle = 210.0 + elif angle_rand > 0.25: + base_angle = 20.0 + else: + base_angle = -30.0 + base_angle += (0.5 - random.random()) * 20.0 + wave = Wave(base_angle=base_angle, entries=all_entries) + return wave + + def add_bot_at_point( + self, spaz_type: type[SpazBot], spawn_time: float = 1.0 + ) -> None: + """Add a new bot at a specified named point.""" + if self._game_over: + return + + def _getpt() -> Sequence[float]: + point = self.map.get_def_points( + 'ffa_spawn')[self._next_ffa_start_index] + self._next_ffa_start_index = ( + self._next_ffa_start_index + 1) % len( + self.map.get_def_points('ffa_spawn') + ) + x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3]) + z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5]) + point = ( + point[0] + random.uniform(*x_range), + point[1], + point[2] + random.uniform(*z_range), + ) + return point + pointpos = _getpt() + + assert self._bots is not None + self._bots.spawn_bot(spaz_type, pos=pointpos, spawn_time=spawn_time) + + def add_bot_at_angle( + self, angle: float, spaz_type: type[SpazBot], spawn_time: float = 1.0 + ) -> None: + """Add a new bot at a specified angle (for circular maps).""" + if self._game_over: + return + angle_radians = angle / 57.2957795 + xval = math.sin(angle_radians) * 1.06 + zval = math.cos(angle_radians) * 1.06 + point = (xval / 0.125, 2.3, (zval / 0.2) - 3.7) + assert self._bots is not None + self._bots.spawn_bot(spaz_type, pos=point, spawn_time=spawn_time) + + def _update_time_bonus(self) -> None: + self._time_bonus = int(self._time_bonus * 0.93) + if self._time_bonus > 0 and self._time_bonus_text is not None: + assert self._time_bonus_text.node + self._time_bonus_text.node.text = babase.Lstr( + value='${A}: ${B}', + subs=[ + ('${A}', babase.Lstr(resource='timeBonusText')), + ('${B}', str(self._time_bonus)), + ], + ) + else: + self._time_bonus_text = None + + def _start_updating_waves(self) -> None: + self._wave_update_timer = bs.Timer( + 2.0, bs.WeakCall(self._update_waves), repeat=True + ) + + def _update_scores(self) -> None: + score = self._score + assert self._scoreboard is not None + self._scoreboard.set_team_value(self.teams[0], score, max_score=None) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, PlayerSpazHurtMessage): + msg.spaz.getplayer(Player, True).has_been_hurt = True + self._a_player_has_been_hurt = True + + elif isinstance(msg, bs.PlayerScoredMessage): + self._score += msg.score + self._update_scores() + + elif isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard behavior. + player = msg.getplayer(Player) + self._a_player_has_been_hurt = True + + # Make note with the player when they can respawn: + if self._wavenum < 10: + player.respawn_wave = max(2, self._wavenum + 1) + elif self._wavenum < 15: + player.respawn_wave = max(2, self._wavenum + 2) + else: + player.respawn_wave = max(2, self._wavenum + 3) + bs.timer(0.1, self._update_player_spawn_info) + bs.timer(0.1, self._checkroundover) + + elif isinstance(msg, SpazBotDiedMessage): + pts, importance = msg.spazbot.get_death_points(msg.how) + if msg.killerplayer is not None: + target: Sequence[float] | None + if msg.spazbot.node: + target = msg.spazbot.node.position + else: + target = None + + killerplayer = msg.killerplayer + self.stats.player_scored( + killerplayer, + pts, + target=target, + kill=True, + screenmessage=False, + importance=importance, + ) + self._dingsound.play( + volume=0.6) if importance == 1 else self._dingsoundhigh.play(volume=0.6) + + # Normally we pull scores from the score-set, but if there's + # no player lets be explicit. + else: + self._score += pts + self._update_scores() + else: + super().handlemessage(msg) + + def _handle_uber_kill_achievements(self, msg: SpazBotDiedMessage) -> None: + + # Uber mine achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): + self._land_mine_kills += 1 + if self._land_mine_kills >= 6: + self._award_achievement('Gold Miner') + + # Uber tnt achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): + self._tnt_kills += 1 + if self._tnt_kills >= 6: + bs.timer( + 0.5, bs.WeakCall(self._award_achievement, 'TNT Terror') + ) + + def _handle_pro_kill_achievements(self, msg: SpazBotDiedMessage) -> None: + + # TNT achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): + self._tnt_kills += 1 + if self._tnt_kills >= 3: + bs.timer( + 0.5, + bs.WeakCall( + self._award_achievement, 'Boom Goes the Dynamite' + ), + ) + + def _handle_rookie_kill_achievements(self, msg: SpazBotDiedMessage) -> None: + # Land-mine achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): + self._land_mine_kills += 1 + if self._land_mine_kills >= 3: + self._award_achievement('Mine Games') + + def _handle_training_kill_achievements( + self, msg: SpazBotDiedMessage + ) -> None: + # Toss-off-map achievement: + if msg.spazbot.last_attacked_type == ('picked_up', 'default'): + self._throw_off_kills += 1 + if self._throw_off_kills >= 3: + self._award_achievement('Off You Go Then') + + def _set_can_end_wave(self) -> None: + self._can_end_wave = True + + def end_game(self) -> None: + # Tell our bots to celebrate just to rub it in. + assert self._bots is not None + self._bots.final_celebrate() + self._game_over = True + self.do_end('defeat', delay=2.0) + bs.setmusic(None) + + def on_continue(self) -> None: + for player in self.players: + if not player.is_alive(): + self.spawn_player(player) + + def _checkroundover(self) -> None: + """Potentially end the round based on the state of the game.""" + if self.has_ended(): + return + if not any(player.is_alive() for player in self.teams[0].players): + # Allow continuing after wave 1. + if self._wavenum > 1: + self.continue_or_end_game() + else: + self.end_game() + +# ba_meta export plugin + + +class CustomOnslaughtLevel(babase.Plugin): + def on_app_running(self) -> None: + babase.app.classic.add_coop_practice_level( + bs._level.Level( + 'Onslaught Football', + gametype=OnslaughtFootballGame, + settings={ + 'map': 'Football Stadium', + 'Epic Mode': False, + }, + preview_texture_name='footballStadiumPreview', + ) + ) + babase.app.classic.add_coop_practice_level( + bs._level.Level( + 'Onslaught Football Epic', + gametype=OnslaughtFootballGame, + settings={ + 'map': 'Football Stadium', + 'Epic Mode': True, + }, + preview_texture_name='footballStadiumPreview', + ) + ) diff --git a/plugins/minigames/quake.py b/plugins/minigames/quake.py new file mode 100644 index 000000000..61f549b59 --- /dev/null +++ b/plugins/minigames/quake.py @@ -0,0 +1,649 @@ +# Porting made easier by baport.(https://github.com/bombsquad-community/baport) +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +"""Quake Game Activity""" +# ba_meta require api 9 +from __future__ import annotations + +from typing import TYPE_CHECKING + +import random +import enum +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase + +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBox +from bascenev1lib.gameutils import SharedObjects + +# from rocket +from bascenev1lib.actor.bomb import Blast + +# from railgun +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.spaz import Spaz + + +if TYPE_CHECKING: + from typing import Optional, List, Any, Type, Union, Sequence + + +STORAGE_ATTR_NAME = f'_shared_{__name__}_factory' + + +# +++++++++++++++++++Rocket++++++++++++++++++++++++ +class RocketFactory: + """Quake Rocket factory""" + + def __init__(self) -> None: + self.ball_material = bs.Material() + + self.ball_material.add_actions( + conditions=((('we_are_younger_than', 5), 'or', + ('they_are_younger_than', 5)), 'and', + ('they_have_material', + SharedObjects.get().object_material)), + actions=('modify_node_collision', 'collide', False)) + + self.ball_material.add_actions( + conditions=('they_have_material', + SharedObjects.get().pickup_material), + actions=('modify_part_collision', 'use_node_collide', False)) + + self.ball_material.add_actions(actions=('modify_part_collision', + 'friction', 0)) + + self.ball_material.add_actions( + conditions=(('they_have_material', + SharedObjects.get().footing_material), 'or', + ('they_have_material', + SharedObjects.get().object_material)), + actions=('message', 'our_node', 'at_connect', ImpactMessage())) + + @classmethod + def get(cls): + """Get factory if exists else create new""" + activity = bs.getactivity() + if hasattr(activity, STORAGE_ATTR_NAME): + return getattr(activity, STORAGE_ATTR_NAME) + factory = cls() + setattr(activity, STORAGE_ATTR_NAME, factory) + return factory + + +class RocketLauncher: + """Very dangerous weapon""" + + def __init__(self): + self.last_shot: Optional[int, float] = 0 + + def give(self, spaz: Spaz) -> None: + """Give spaz a rocket launcher""" + spaz.punch_callback = self.shot + self.last_shot = bs.time() + + # FIXME + # noinspection PyUnresolvedReferences + def shot(self, spaz: Spaz) -> None: + """Release a rocket""" + time = bs.time() + if time - self.last_shot > 0.6: + self.last_shot = time + center = spaz.node.position_center + forward = spaz.node.position_forward + direction = [center[0] - forward[0], forward[1] - center[1], + center[2] - forward[2]] + direction[1] = 0.0 + + mag = 10.0 / babase.Vec3(*direction).length() + vel = [v * mag for v in direction] + Rocket(position=spaz.node.position, + velocity=vel, + owner=spaz.getplayer(bs.Player), + source_player=spaz.getplayer(bs.Player), + color=spaz.node.color).autoretain() + + +class ImpactMessage: + """Rocket touched something""" + + +class Rocket(bs.Actor): + """Epic rocket from rocket launcher""" + + def __init__(self, + position=(0, 5, 0), + velocity=(1, 0, 0), + source_player=None, + owner=None, + color=(1.0, 0.2, 0.2)) -> None: + super().__init__() + self.source_player = source_player + self.owner = owner + self._color = color + factory = RocketFactory.get() + + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'mesh': bs.getmesh('impactBomb'), + 'body': 'sphere', + 'color_texture': bs.gettexture( + 'bunnyColor'), + 'mesh_scale': 0.2, + 'is_area_of_interest': True, + 'body_scale': 0.8, + 'materials': [ + SharedObjects.get().object_material, + factory.ball_material] + }) # yapf: disable + self.node.extra_acceleration = (self.node.velocity[0] * 200, 0, + self.node.velocity[2] * 200) + + self._life_timer = bs.Timer( + 5, bs.WeakCall(self.handlemessage, bs.DieMessage())) + + self._emit_timer = bs.Timer(0.001, bs.WeakCall(self.emit), repeat=True) + self.base_pos_y = self.node.position[1] + + bs.camerashake(5.0) + + def emit(self) -> None: + """Emit a trace after rocket""" + bs.emitfx(position=self.node.position, + scale=0.4, + spread=0.01, + chunk_type='spark') + if not self.node: + return + self.node.position = (self.node.position[0], self.base_pos_y, + self.node.position[2]) # ignore y + bs.newnode('explosion', + owner=self.node, + attrs={ + 'position': self.node.position, + 'radius': 0.2, + 'color': self._color + }) + + def handlemessage(self, msg: Any) -> Any: + """Message handling for rocket""" + super().handlemessage(msg) + if isinstance(msg, ImpactMessage): + self.node.handlemessage(bs.DieMessage()) + + elif isinstance(msg, bs.DieMessage): + if self.node: + Blast(position=self.node.position, + blast_radius=2, + source_player=self.source_player) + + self.node.delete() + self._emit_timer = None + + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + +# -------------------Rocket-------------------------- + + +# ++++++++++++++++++Railgun++++++++++++++++++++++++++ +class Railgun: + """Very dangerous weapon""" + + def __init__(self) -> None: + self.last_shot: Optional[int, float] = 0 + + def give(self, spaz: Spaz) -> None: + """Give spaz a railgun""" + spaz.punch_callback = self.shot + self.last_shot = bs.time() + + # FIXME + # noinspection PyUnresolvedReferences + def shot(self, spaz: Spaz) -> None: + """Release a rocket""" + time = bs.time() + if time - self.last_shot > 0.6: + self.last_shot = time + center = spaz.node.position_center + forward = spaz.node.position_forward + direction = [ + center[0] - forward[0], forward[1] - center[1], + center[2] - forward[2] + ] + direction[1] = 0.0 + + RailBullet(position=spaz.node.position, + direction=direction, + owner=spaz.getplayer(bs.Player), + source_player=spaz.getplayer(bs.Player), + color=spaz.node.color).autoretain() + + +class TouchedToSpazMessage: + """I hit!""" + + def __init__(self, spaz) -> None: + self.spaz = spaz + + +class RailBullet(bs.Actor): + """Railgun bullet""" + + def __init__(self, + position=(0, 5, 0), + direction=(0, 2, 0), + source_player=None, + owner=None, + color=(1, 1, 1)) -> None: + super().__init__() + self._color = color + + self.node = bs.newnode('light', + delegate=self, + attrs={ + 'position': position, + 'color': self._color + }) + bs.animate(self.node, 'radius', {0: 0, 0.1: 0.5, 0.5: 0}) + + self.source_player = source_player + self.owner = owner + self._life_timer = bs.Timer( + 0.5, bs.WeakCall(self.handlemessage, bs.DieMessage())) + + pos = position + vel = tuple(i / 5 for i in babase.Vec3(direction).normalized()) + for _ in range(500): # Optimization :( + bs.newnode('explosion', + owner=self.node, + attrs={ + 'position': pos, + 'radius': 0.2, + 'color': self._color + }) + pos = (pos[0] + vel[0], pos[1] + vel[1], pos[2] + vel[2]) + + for node in bs.getnodes(): + if node and node.getnodetype() == 'spaz': + # pylint: disable=invalid-name + m3 = babase.Vec3(position) + a = babase.Vec3(direction[2], direction[1], direction[0]) + m1 = babase.Vec3(node.position) + # pylint: enable=invalid-name + # distance between node and line + dist = (a * (m1 - m3)).length() / a.length() + if dist < 0.3: + if node and node != self.owner and node.getdelegate( + PlayerSpaz, True).getplayer( + bs.Player, True).team != self.owner.team: + node.handlemessage(bs.FreezeMessage()) + pos = self.node.position + hit_dir = (0, 10, 0) + + node.handlemessage( + bs.HitMessage(pos=pos, + magnitude=50, + velocity_magnitude=50, + radius=0, + srcnode=self.node, + source_player=self.source_player, + force_direction=hit_dir)) + + def handlemessage(self, msg: Any) -> Any: + super().handlemessage(msg) + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + +# ------------------Railgun------------------------- + + +class Player(bs.Player['Team']): + """Our player""" + + +class Team(bs.Team[Player]): + """Our team""" + + def __init__(self) -> None: + self.score = 0 + + +class WeaponType(enum.Enum): + """Type of weapon""" + ROCKET = 0 + RAILGUN = 1 + + +class ObstaclesForm(enum.Enum): + """Obstacle form""" + CUBE = 0 + SPHERE = 1 + RANDOM = 2 + + +# ba_meta export bascenev1.GameActivity +class QuakeGame(bs.TeamGameActivity[Player, Team]): + """Quake Team Game Activity""" + name = 'Quake' + description = 'Kill a set number of enemies to win.' + available_settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + default=15, + min_value=1, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[('None', 0), ('1 Minute', 60), ('2 Minutes', 120), + ('5 Minutes', 300), ('10 Minutes', 600), + ('20 Minutes', 1200)], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[('At once', 0.0), ('Shorter', 0.25), ('Short', 0.5), + ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], + default=1.0, + ), + bs.BoolSetting( + 'Speed', + default=True, + ), + bs.BoolSetting( + 'Enable Jump', + default=True, + ), + bs.BoolSetting( + 'Enable Pickup', + default=True, + ), + bs.BoolSetting( + 'Enable Bomb', + default=False, + ), + bs.BoolSetting( + 'Obstacles', + default=True, + ), + bs.IntChoiceSetting( + 'Obstacles Form', + choices=[('Cube', ObstaclesForm.CUBE.value), + ('Sphere', ObstaclesForm.SPHERE.value), + ('Random', ObstaclesForm.RANDOM.value)], + default=0, + ), + bs.IntChoiceSetting( + 'Weapon Type', + choices=[('Rocket', WeaponType.ROCKET.value), + ('Railgun', WeaponType.RAILGUN.value)], + default=WeaponType.ROCKET.value, + ), + bs.BoolSetting( + 'Obstacles Mirror Shots', + default=False, + ), + bs.IntSetting( + 'Obstacles Count', + default=16, + min_value=0, + increment=2, + ), + bs.BoolSetting( + 'Random Obstacles Color', + default=True, + ), + bs.BoolSetting( + 'Epic Mode', + default=False, + ), + ] + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.MultiTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + # TODO add more maps + return ['Football Stadium', 'Monkey Face', 'Doom Shroom'] + + def __init__(self, settings) -> None: + super().__init__(settings) + self._epic_mode = self.settings_raw['Epic Mode'] + self._score_to_win = self.settings_raw['Kills to Win Per Player'] + self._time_limit = self.settings_raw['Time Limit'] + self._obstacles_enabled = self.settings_raw['Obstacles'] + self._obstacles_count = self.settings_raw['Obstacles Count'] + self._speed_enabled = self.settings_raw['Speed'] + self._bomb_enabled = self.settings_raw['Enable Bomb'] + self._pickup_enabled = self.settings_raw['Enable Pickup'] + self._jump_enabled = self.settings_raw['Enable Jump'] + self._weapon_type = WeaponType(self.settings_raw['Weapon Type']) + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.GRAND_ROMP) + self.slow_motion = self._epic_mode + + self.announce_player_deaths = True + self._scoreboard = Scoreboard() + self._ding_sound = bs.getsound('dingSmall') + + self._shield_dropper: Optional[bs.Timer] = None + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Kill ${ARG1} enemies.', self._score_to_win + + def on_team_join(self, team: Team) -> None: + team.score = 0 + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + bs.TeamGameActivity.on_begin(self) + bs.getactivity().globalsnode.tint = (0.5, 0.7, 1) + self.drop_shield() + self._shield_dropper = bs.Timer(8, + bs.WeakCall(self.drop_shield), + repeat=True) + self.setup_standard_time_limit(self._time_limit) + if self._obstacles_enabled: + count = self._obstacles_count + gamemap = self.map.getname() + for i in range(count): # TODO: tidy up around here + if gamemap == 'Football Stadium': + radius = (random.uniform(-10, 1), + 6, + random.uniform(-4.5, 4.5)) \ + if i > count / 2 else ( + random.uniform(10, 1), 6, random.uniform(-4.5, 4.5)) + else: + radius = (random.uniform(-10, 1), + 6, + random.uniform(-8, 8)) \ + if i > count / 2 else ( + random.uniform(10, 1), 6, random.uniform(-8, 8)) + + Obstacle( + position=radius, + mirror=self.settings_raw['Obstacles Mirror Shots'], + form=self.settings_raw['Obstacles Form']).autoretain() + + self._update_scoreboard() + + def drop_shield(self) -> None: + """Drop a shield powerup in random place""" + # FIXME: should use map defs + shield = PowerupBox(poweruptype='shield', + position=(random.uniform(-10, 10), 6, + random.uniform(-5, 5))).autoretain() + + self._ding_sound.play() + + p_light = bs.newnode('light', + owner=shield.node, + attrs={ + 'position': (0, 0, 0), + 'color': (0.3, 0.0, 0.4), + 'radius': 0.3, + 'intensity': 2, + 'volume_intensity_scale': 10.0 + }) + + shield.node.connectattr('position', p_light, 'position') + + bs.animate(p_light, 'intensity', {0: 2, 8: 0}) + + def spawn_player(self, player: Player) -> None: + spaz = self.spawn_player_spaz(player) + if self._weapon_type == WeaponType.ROCKET: + RocketLauncher().give(spaz) + elif self._weapon_type == WeaponType.RAILGUN: + Railgun().give(spaz) + spaz.connect_controls_to_player(enable_jump=self._jump_enabled, + enable_pickup=self._pickup_enabled, + enable_bomb=self._bomb_enabled, + enable_fly=False) + + spaz.node.hockey = self._speed_enabled + spaz.spaz_light = bs.newnode('light', + owner=spaz.node, + attrs={ + 'position': (0, 0, 0), + 'color': spaz.node.color, + 'radius': 0.12, + 'intensity': 1, + 'volume_intensity_scale': 10.0 + }) + + spaz.node.connectattr('position', spaz.spaz_light, 'position') + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + bs.TeamGameActivity.handlemessage(self, msg) + player = msg.getplayer(Player) + self.respawn_player(player) + killer = msg.getkillerplayer(Player) + if killer is None: + return + + # handle team-kills + if killer.team is player.team: + # in free-for-all, killing yourself loses you a point + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + new_score = max(0, new_score) + player.team.score = new_score + # in teams-mode it gives a point to the other team + else: + self._ding_sound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + # killing someone on another team nets a kill + else: + killer.team.score += 1 + self._ding_sound.play() + # in FFA show our score since its hard to find on + # the scoreboard + assert killer.actor is not None + # noinspection PyUnresolvedReferences + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # if someone has won, set a timer to end shortly + # (allows the dust to clear and draws to occur if + # deaths are close enough) + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + else: + bs.TeamGameActivity.handlemessage(self, msg) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + + self.end(results=results) + + +class Obstacle(bs.Actor): + """Scene object""" + + def __init__(self, + position, + form=ObstaclesForm.CUBE, + mirror=False) -> None: + bs.Actor.__init__(self) + + if form == ObstaclesForm.CUBE: + mesh = 'tnt' + body = 'crate' + elif form == ObstaclesForm.SPHERE: + mesh = 'bomb' + body = 'sphere' + else: # ObstaclesForm.RANDOM: + mesh = random.choice(['tnt', 'bomb']) + body = 'sphere' if mesh == 'bomb' else 'crate' + + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'position': + position, + 'mesh': + bs.getmesh(mesh), + 'body': + body, + 'body_scale': + 1.3, + 'mesh_scale': + 1.3, + 'reflection': + 'powerup', + 'reflection_scale': [0.7], + 'color_texture': + bs.gettexture('bunnyColor'), + 'materials': [SharedObjects.get().footing_material] + if mirror else [ + SharedObjects.get().object_material, + SharedObjects.get().footing_material + ] + }) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, bs.OutOfBoundsMessage): + if self.node: + self.handlemessage(bs.DieMessage()) + + elif isinstance(msg, bs.HitMessage): + self.node.handlemessage('impulse', msg.pos[0], msg.pos[1], + msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], + msg.magnitude, msg.velocity_magnitude, + msg.radius, 0, msg.velocity[0], + msg.velocity[1], msg.velocity[2]) diff --git a/plugins/minigames/quake_original.py b/plugins/minigames/quake_original.py new file mode 100644 index 000000000..84ff0f687 --- /dev/null +++ b/plugins/minigames/quake_original.py @@ -0,0 +1,624 @@ +# Created By Idk +# Ported to 1.7 by Yan + +# ba_meta require api 8 +from __future__ import annotations + +from typing import TYPE_CHECKING + +from bascenev1lib.actor.powerupbox import PowerupBox as Powerup +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects + +import bascenev1lib.actor.bomb +import bascenev1lib.actor.spaz +import weakref +import random +import math +import babase +import bauiv1 as bui +import bascenev1 as bs + +if TYPE_CHECKING: + pass + + +class TouchedToSpaz(object): + pass + + +class TouchedToAnything(object): + pass + + +class TouchedToFootingMaterial(object): + pass + + +class QuakeBallFactory(object): + """Components used by QuakeBall stuff + + category: Game Classes + + """ + _STORENAME = babase.storagename() + + @classmethod + def get(cls) -> QuakeBallFactory: + """Get/create a shared bascenev1lib.actor.bomb.BombFactory object.""" + activity = bs.getactivity() + factory = activity.customdata.get(cls._STORENAME) + if factory is None: + factory = QuakeBallFactory() + activity.customdata[cls._STORENAME] = factory + assert isinstance(factory, QuakeBallFactory) + return factory + + def __init__(self): + shared = SharedObjects.get() + + self.ball_material = bs.Material() + + self.ball_material.add_actions( + conditions=((('we_are_younger_than', 5), 'or', ('they_are_younger_than', 50)), + 'and', ('they_have_material', shared.object_material)), + actions=(('modify_node_collision', 'collide', False))) + + self.ball_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=(('modify_part_collision', 'use_node_collide', False))) + + self.ball_material.add_actions( + actions=('modify_part_collision', 'friction', 0)) + + self.ball_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'our_node', 'at_connect', TouchedToSpaz()))) + + self.ball_material.add_actions( + conditions=(('they_dont_have_material', shared.player_material), 'and', + ('they_have_material', shared.object_material)), + actions=('message', 'our_node', 'at_connect', TouchedToAnything())) + + self.ball_material.add_actions( + conditions=(('they_dont_have_material', shared.player_material), 'and', + ('they_have_material', shared.footing_material)), + actions=('message', 'our_node', 'at_connect', TouchedToFootingMaterial())) + + def give(self, spaz): + spaz.punch_callback = self.shot + self.last_shot = int(bs.time() * 1000) + + def shot(self, spaz): + time = int(bs.time() * 1000) + if time - self.last_shot > 0.6: + self.last_shot = time + p1 = spaz.node.position_center + p2 = spaz.node.position_forward + direction = [p1[0]-p2[0], p2[1]-p1[1], p1[2]-p2[2]] + direction[1] = 0.0 + + mag = 10.0/babase.Vec3(*direction).length() + vel = [v * mag for v in direction] + QuakeBall( + position=spaz.node.position, + velocity=(vel[0]*2, vel[1]*2, vel[2]*2), + owner=spaz._player, + source_player=spaz._player, + color=spaz.node.color).autoretain() + + +class QuakeBall(bs.Actor): + + def __init__(self, + position=(0, 5, 0), + velocity=(0, 2, 0), + source_player=None, + owner=None, + color=(random.random(), random.random(), random.random()), + light_radius=0 + ): + super().__init__() + + shared = SharedObjects.get() + b_shared = QuakeBallFactory.get() + + self.source_player = source_player + self.owner = owner + + self.node = bs.newnode('prop', delegate=self, attrs={ + 'position': position, + 'velocity': velocity, + 'mesh': bs.getmesh('impactBomb'), + 'body': 'sphere', + 'color_texture': bs.gettexture('bunnyColor'), + 'mesh_scale': 0.2, + 'is_area_of_interest': True, + 'body_scale': 0.8, + 'materials': [shared.object_material, + b_shared.ball_material]}) + + self.light_node = bs.newnode('light', attrs={ + 'position': position, + 'color': color, + 'radius': 0.1+light_radius, + 'volume_intensity_scale': 15.0}) + + self.node.connectattr('position', self.light_node, 'position') + self.emit_time = bs.Timer(0.015, bs.WeakCall(self.emit), repeat=True) + self.life_time = bs.Timer(5.0, bs.WeakCall(self.handlemessage, bs.DieMessage())) + + def emit(self): + bs.emitfx( + position=self.node.position, + velocity=self.node.velocity, + count=10, + scale=0.4, + spread=0.01, + chunk_type='spark') + + def handlemessage(self, m): + if isinstance(m, TouchedToAnything): + node = bs.getcollision().opposingnode + if node is not None and node.exists(): + v = self.node.velocity + t = self.node.position + hitdir = self.node.velocity + m = self.node + node.handlemessage( + bs.HitMessage( + pos=t, + velocity=v, + magnitude=babase.Vec3(*v).length()*40, + velocity_magnitude=babase.Vec3(*v).length()*40, + radius=0, + srcnode=self.node, + source_player=self.source_player, + force_direction=hitdir)) + + self.node.handlemessage(bs.DieMessage()) + + elif isinstance(m, bs.DieMessage): + if self.node.exists(): + velocity = self.node.velocity + explosion = bs.newnode('explosion', attrs={ + 'position': self.node.position, + 'velocity': (velocity[0], max(-1.0, velocity[1]), velocity[2]), + 'radius': 1, + 'big': False}) + + bs.getsound(random.choice(['impactHard', 'impactHard2', 'impactHard3'])).play(), + position = self.node.position + + self.emit_time = None + self.light_node.delete() + self.node.delete() + + elif isinstance(m, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + + elif isinstance(m, bs.HitMessage): + self.node.handlemessage('impulse', m.pos[0], m.pos[1], m.pos[2], + m.velocity[0], m.velocity[1], m.velocity[2], + 1.0*m.magnitude, 1.0*m.velocity_magnitude, m.radius, 0, + m.force_direction[0], m.force_direction[1], m.force_direction[2]) + + elif isinstance(m, TouchedToSpaz): + node = bs.getcollision() .opposingnode + if node is not None and node.exists() and node != self.owner \ + and node.getdelegate(object)._player.team != self.owner.team: + node.handlemessage(bs.FreezeMessage()) + v = self.node.velocity + t = self.node.position + hitdir = self.node.velocity + + node.handlemessage( + bs.HitMessage( + pos=t, + velocity=(10, 10, 10), + magnitude=50, + velocity_magnitude=50, + radius=0, + srcnode=self.node, + source_player=self.source_player, + force_direction=hitdir)) + + self.node.handlemessage(bs.DieMessage()) + + elif isinstance(m, TouchedToFootingMaterial): + bs.getsound('blip').play(), + position = self.node.position + else: + super().handlemessage(m) + + +class Player(bs.Player['Team']): + ... + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + +# ba_meta export bascenev1.GameActivity + + +class QuakeGame(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Quake' + description = 'Kill a set number of enemies to win.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Doom Shroom', 'Monkey Face', 'Football Stadium'] + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.IntChoiceSetting( + 'Graphics', + choices=[ + ('Normal', 1), + ('High', 2) + ], + default=1), + bs.BoolSetting('Fast Movespeed', default=True), + bs.BoolSetting('Enable Jump', default=False), + bs.BoolSetting('Enable Pickup', default=False), + bs.BoolSetting('Enable Bomb', default=False), + bs.BoolSetting('Obstacles', default=False), + bs.IntChoiceSetting( + 'Obstacles Shape', + choices=[ + ('Cube', 1), + ('Sphere', 2), + ('Puck', 3), + ('Egg', 4), + ('Random', 5), + ], + default=1), + bs.BoolSetting('Obstacles Bounces Shots', default=False), + bs.IntSetting( + 'Obstacle Count', + min_value=1, + default=16, + increment=1, + ), + bs.BoolSetting('Random Obstacle Color', default=True), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: int | None = None + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int(settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False) + ) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.TO_THE_DEATH + ) + self.settings = settings + + def get_instance_description(self) -> str | Sequence: + return 'Crush ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> str | Sequence: + return 'kill ${ARG1} enemies', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.dingsound = bs.getsound('dingSmall') + self.setup_standard_time_limit(self._time_limit) + + self.drop_shield() + self.drop_shield_timer = bs.Timer(8.001, bs.WeakCall(self.drop_shield), repeat=True) + + shared = SharedObjects.get() + if self.settings['Obstacles']: + count = self.settings['Obstacle Count'] + map = bs.getactivity()._map.getname() + for i in range(count): + if map == 'Football Stadium': + radius = (random.uniform(-10, 1), + 6, + random.uniform(-4.5, 4.5)) \ + if i > count/2 else (random.uniform(10, 1), 6, random.uniform(-4.5, 4.5)) + else: + radius = (random.uniform(-10, 1), + 6, + random.uniform(-8, 8)) \ + if i > count/2 else (random.uniform(10, 1), 6, random.uniform(-8, 8)) + + Obstacle( + position=radius, + graphics=self.settings['Graphics'], + random_color=self.settings['Random Obstacle Color'], + rebound=self.settings['Obstacles Bounces Shots'], + shape=int(self.settings['Obstacles Shape'])).autoretain() + + if self.settings['Graphics'] == 2: + bs.getactivity().globalsnode.tint = (bs.getactivity( + ).globalsnode.tint[0]-0.6, bs.getactivity().globalsnode.tint[1]-0.6, bs.getactivity().globalsnode.tint[2]-0.6) + light = bs.newnode('light', attrs={ + 'position': (9, 10, 0) if map == 'Football Stadium' else (6, 7, -2) + if not map == 'Rampage' else (6, 11, -2) if not map == 'The Pad' else (6, 8.5, -2), + 'color': (0.4, 0.4, 0.45), + 'radius': 1, + 'intensity': 6, + 'volume_intensity_scale': 10.0}) + + light2 = bs.newnode('light', attrs={ + 'position': (-9, 10, 0) if map == 'Football Stadium' else (-6, 7, -2) + if not map == 'Rampage' else (-6, 11, -2) if not map == 'The Pad' else (-6, 8.5, -2), + 'color': (0.4, 0.4, 0.45), + 'radius': 1, + 'intensity': 6, + 'volume_intensity_scale': 10.0}) + + if len(self.teams) > 0: + self._score_to_win = self.settings['Kills to Win Per Player'] * \ + max(1, max(len(t.players) for t in self.teams)) + else: + self._score_to_win = self.settings['Kills to Win Per Player'] + self._update_scoreboard() + + def drop_shield(self): + p = Powerup( + poweruptype='shield', + position=(random.uniform(-10, 10), 6, random.uniform(-5, 5))).autoretain() + + bs.getsound('dingSmall').play() + + p_light = bs.newnode('light', attrs={ + 'position': (0, 0, 0), + 'color': (0.3, 0.0, 0.4), + 'radius': 0.3, + 'intensity': 2, + 'volume_intensity_scale': 10.0}) + + p.node.connectattr('position', p_light, 'position') + + bs.animate(p_light, 'intensity', {0: 2, 8000: 0}) + + def check_exists(): + if p is None or p.node.exists() == False: + delete_light() + del_checker() + + self._checker = bs.Timer(0.1, babase.Call(check_exists), repeat=True) + + def del_checker(): + if self._checker is not None: + self._checker = None + + def delete_light(): + if p_light.exists(): + p_light.delete() + + bs.timer(6.9, babase.Call(del_checker)) + bs.timer(7.0, babase.Call(delete_light)) + + def spawn_player(self, player: bs.Player): + spaz = self.spawn_player_spaz(player) + QuakeBallFactory().give(spaz) + spaz.connect_controls_to_player( + enable_jump=self.settings['Enable Jump'], + enable_punch=True, + enable_pickup=self.settings['Enable Pickup'], + enable_bomb=self.settings['Enable Bomb'], + enable_run=True, + enable_fly=False) + + if self.settings['Fast Movespeed']: + spaz.node.hockey = True + spaz.spaz_light = bs.newnode('light', attrs={ + 'position': (0, 0, 0), + 'color': spaz.node.color, + 'radius': 0.12, + 'intensity': 1, + 'volume_intensity_scale': 10.0}) + + spaz.node.connectattr('position', spaz.spaz_light, 'position') + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if hasattr(player.actor, 'spaz_light'): + player.actor.spaz_light.delete() + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + self._dingsound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + self._dingsound.play() + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text( + str(killer.team.score) + '/' + str(self._score_to_win), + color=killer.team.color, + flash=True, + ) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value( + team, team.score, self._score_to_win + ) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + +class Obstacle(bs.Actor): + + def __init__(self, + position: tuple(float, float, float), + graphics: bool, + random_color: bool, + rebound: bool, + shape: int) -> None: + super().__init__() + + shared = SharedObjects.get() + if shape == 1: + mesh = 'tnt' + body = 'crate' + elif shape == 2: + mesh = 'bomb' + body = 'sphere' + elif shape == 3: + mesh = 'puck' + body = 'puck' + elif shape == 4: + mesh = 'egg' + body = 'capsule' + elif shape == 5: + pair = random.choice([ + {'mesh': 'tnt', 'body': 'crate'}, + {'mesh': 'bomb', 'body': 'sphere'}, + {'mesh': 'puckModel', 'body': 'puck'}, + {'mesh': 'egg', 'body': 'capsule'} + ]) + mesh = pair['mesh'] + body = pair['body'] + + self.node = bs.newnode('prop', delegate=self, attrs={ + 'position': position, + 'mesh': bs.getmesh(mesh), + 'body': body, + 'body_scale': 1.3, + 'mesh_scale': 1.3, + 'reflection': 'powerup', + 'reflection_scale': [0.7], + 'color_texture': bs.gettexture('bunnyColor'), + 'materials': [shared.footing_material if rebound else shared.object_material, + shared.footing_material]}) + + if graphics == 2: + self.light_node = bs.newnode('light', attrs={ + 'position': (0, 0, 0), + 'color': ((0.8, 0.2, 0.2) if i < count/2 else (0.2, 0.2, 0.8)) + if not random_color else ((random.uniform(0, 1.1), random.uniform(0, 1.1), random.uniform(0, 1.1))), + 'radius': 0.2, + 'intensity': 1, + 'volume_intensity_scale': 10.0}) + + self.node.connectattr('position', self.light_node, 'position') + + def handlemessage(self, m): + if isinstance(m, bs.DieMessage): + if self.node.exists(): + if hasattr(self, 'light_node'): + self.light_node.delete() + self.node.delete() + + elif isinstance(m, bs.OutOfBoundsMessage): + if self.node.exists(): + self.handlemessage(bs.DieMessage()) + + elif isinstance(m, bs.HitMessage): + self.node.handlemessage('impulse', m.pos[0], m.pos[1], m.pos[2], + m.velocity[0], m.velocity[1], m.velocity[2], + m.magnitude, m.velocity_magnitude, m.radius, 0, + m.velocity[0], m.velocity[1], m.velocity[2]) diff --git a/plugins/minigames/shimla.py b/plugins/minigames/shimla.py new file mode 100644 index 000000000..231619447 --- /dev/null +++ b/plugins/minigames/shimla.py @@ -0,0 +1,411 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# +"""DeathMatch game and support classes.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.game.deathmatch import DeathMatchGame, Player +from bascenev1lib.gameutils import SharedObjects +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + +# ba_meta export bascenev1.GameActivity + + +class ShimlaGame(DeathMatchGame): + name = 'Shimla' + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Creative Thoughts'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self.lifts = {} + self._real_wall_material = bs.Material() + self._real_wall_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + + self._real_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._lift_material = bs.Material() + self._lift_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._lift_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', self._handle_lift),), + ) + self._lift_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_disconnect', self._handle_lift_disconnect),), + ) + + def on_begin(self): + bs.getactivity().globalsnode.happy_thoughts_mode = False + super().on_begin() + + self.make_map() + bs.timer(2, self.disable_fly) + + def disable_fly(self): + activity = bs.get_foreground_host_activity() + + for players in activity.players: + players.actor.node.fly = False + + def spawn_player_spaz( + self, + player: Player, + position: Sequence[float] | None = None, + angle: float | None = None, + ) -> PlayerSpaz: + """Intercept new spazzes and add our team material for them.""" + spaz = super().spawn_player_spaz(player, position, angle) + + spaz.connect_controls_to_player(enable_punch=True, + enable_bomb=True, + enable_pickup=True, + enable_fly=False, + enable_jump=True) + spaz.fly = False + return spaz + + def make_map(self): + shared = SharedObjects.get() + bs.get_foreground_host_activity()._map.leftwall.materials = [ + shared.footing_material, self._real_wall_material] + + bs.get_foreground_host_activity()._map.rightwall.materials = [ + shared.footing_material, self._real_wall_material] + + bs.get_foreground_host_activity()._map.topwall.materials = [ + shared.footing_material, self._real_wall_material] + + self.floorwall1 = bs.newnode('region', attrs={'position': (-10, 5, -5.52), 'scale': + (15, 0.2, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.floorwall2 = bs.newnode('region', attrs={'position': (10, 5, -5.52), 'scale': ( + 15, 0.2, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + + self.wall1 = bs.newnode('region', attrs={'position': (0, 11, -6.90), 'scale': ( + 35.4, 20, 1), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.wall2 = bs.newnode('region', attrs={'position': (0, 11, -4.14), 'scale': ( + 35.4, 20, 1), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + + bs.newnode('locator', attrs={'shape': 'box', 'position': (-10, 5, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (15, 0.2, 2)}) + + bs.newnode('locator', attrs={'shape': 'box', 'position': (10, 5, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (15, 0.2, 2)}) + self.create_lift(-16.65, 8) + + self.create_lift(16.65, 8) + + self.create_static_step(0, 18.29) + self.create_static_step(0, 7) + + self.create_static_step(13, 17) + self.create_static_step(-13, 17) + self.create_slope(8, 15, True) + self.create_slope(-8, 15, False) + self.create_static_step(5, 15) + self.create_static_step(-5, 15) + + self.create_static_step(13, 12) + self.create_static_step(-13, 12) + self.create_slope(8, 10, True) + self.create_slope(-8, 10, False) + self.create_static_step(5, 10) + self.create_static_step(-5, 10) + + def create_static_step(self, x, y): + + shared = SharedObjects.get() + + bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': (5.5, 0.1, 6), + 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (x, y, -5.52), 'color': ( + 1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (5.5, 0.1, 2)}) + + def create_lift(self, x, y): + shared = SharedObjects.get() + color = (0.7, 0.6, 0.5) + + floor = bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': ( + 1.8, 0.1, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material, self._lift_material]}) + + cleaner = bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': ( + 2, 0.3, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + + lift = bs.newnode('locator', attrs={'shape': 'box', 'position': ( + x, y, -5.52), 'color': color, 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (1.8, 3.7, 2)}) + + _tcombine = bs.newnode('combine', + owner=floor, + attrs={ + 'input0': x, + 'input2': -5.5, + 'size': 3 + }) + mnode = bs.newnode('math', + owner=lift, + attrs={ + 'input1': (0, 2, 0), + 'operation': 'add' + }) + _tcombine.connectattr('output', mnode, 'input2') + + _cleaner_combine = bs.newnode('combine', + owner=cleaner, + attrs={ + 'input1': 5.6, + 'input2': -5.5, + 'size': 3 + }) + _cleaner_combine.connectattr('output', cleaner, 'position') + bs.animate(_tcombine, 'input1', { + 0: 5.1, + }) + bs.animate(_cleaner_combine, 'input0', { + 0: -19 if x < 0 else 19, + }) + + _tcombine.connectattr('output', floor, 'position') + mnode.connectattr('output', lift, 'position') + self.lifts[floor] = {"state": "origin", "lift": _tcombine, + "cleaner": _cleaner_combine, 'leftLift': x < 0} + + def _handle_lift(self): + region = bs.getcollision().sourcenode + lift = self.lifts[region] + + def clean(lift): + bs.animate(lift["cleaner"], 'input0', { + 0: -19 if lift["leftLift"] else 19, + 2: -16 if lift["leftLift"] else 16, + 4.3: -19 if lift["leftLift"] else 19 + }) + if lift["state"] == "origin": + lift["state"] = "transition" + bs.animate(lift["lift"], 'input1', { + 0: 5.1, + 1.3: 5.1, + 6: 5+12, + 9: 5+12, + 15: 5.1 + }) + bs.timer(16, babase.Call(lambda lift: lift.update({'state': 'end'}), lift)) + bs.timer(12, babase.Call(clean, lift)) + + def _handle_lift_disconnect(self): + region = bs.getcollision().sourcenode + lift = self.lifts[region] + if lift["state"] == 'end': + lift["state"] = "origin" + + def create_slope(self, x, y, backslash): + shared = SharedObjects.get() + + for i in range(0, 21): + bs.newnode('region', attrs={'position': (x, y, -5.52), 'scale': (0.2, 0.1, 6), + 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (x, y, -5.52), 'color': ( + 1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.2, 0.1, 2)}) + if backslash: + x = x+0.1 + y = y+0.1 + else: + x = x-0.1 + y = y+0.1 + + +class mapdefs: + points = {} + # noinspection PyDictCreation + boxes = {} + boxes['area_of_interest_bounds'] = (-1.045859963, 12.67722855, + -5.401537075) + (0.0, 0.0, 0.0) + ( + 42.46156851, 20.94044653, 0.6931564611) + points['ffa_spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) + points['ffa_spawn2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) + points['ffa_spawn3'] = (9.55724115, 11.30789446, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn4'] = (-11.55747023, 10.99170684, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn5'] = (-1.878892369, 9.46490571, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn6'] = (-0.4912812943, 5.077006397, -5.521672101) + ( + 1.878332089, 1.453808816, 0.007578097856) + points['flag1'] = (-11.75152479, 8.057427485, -5.52) + points['flag2'] = (9.840909039, 8.188634282, -5.52) + points['flag3'] = (-0.2195258696, 5.010273907, -5.52) + points['flag4'] = (-0.04605809154, 12.73369108, -5.52) + points['flag_default'] = (-0.04201942896, 12.72374492, -5.52) + boxes['map_bounds'] = (-0.8748348681, 9.212941713, -5.729538885) + ( + 0.0, 0.0, 0.0) + (42.09666006, 26.19950145, 7.89541168) + points['powerup_spawn1'] = (1.160232442, 6.745963662, -5.469115985) + points['powerup_spawn2'] = (-1.899700206, 10.56447241, -5.505721177) + points['powerup_spawn3'] = (10.56098871, 12.25165669, -5.576232453) + points['powerup_spawn4'] = (-12.33530337, 12.25165669, -5.576232453) + points['spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) + points['spawn2'] = (7.484707127, 8.172681752, + -5.614479365) + (1.553861796, 1.453808816, 0.04419853907) + points['spawn_by_flag1'] = (-9.295167711, 8.010664315, -5.44451005) + ( + 1.555840357, 1.453808816, 0.1165648888) + points['spawn_by_flag2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) + points['spawn_by_flag3'] = (-1.45994593, 5.038762459, -5.535288724) + ( + 0.9516389866, 0.6666414677, 0.08607244075) + points['spawn_by_flag4'] = (0.4932087091, 12.74493212, -5.598987003) + ( + 0.5245740665, 0.5245740665, 0.01941146064) + + +class CreativeThoughts(bs.Map): + """Freaking map by smoothy.""" + + defs = mapdefs + + name = 'Creative Thoughts' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [ + 'melee', 'keep_away', 'team_flag' + ] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'alwaysLandPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'mesh': bs.getmesh('alwaysLandLevel'), + 'bottom_mesh': bs.getmesh('alwaysLandLevelBottom'), + 'bgmesh': bs.getmesh('alwaysLandBG'), + 'collision_mesh': bs.getcollisionmesh('alwaysLandLevelCollide'), + 'tex': bs.gettexture('alwaysLandLevelColor'), + 'bgtex': bs.gettexture('alwaysLandBGColor'), + 'vr_fill_mound_mesh': bs.getmesh('alwaysLandVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound') + } + return data + + @classmethod + def get_music_type(cls) -> bs.MusicType: + return bs.MusicType.FLYING + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, -3.7, 2.5)) + shared = SharedObjects.get() + self._fake_wall_material = bs.Material() + self._real_wall_material = bs.Material() + self._fake_wall_material.add_actions( + conditions=(('they_are_younger_than', 9000), 'and', + ('they_have_material', shared.player_material)), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._real_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + 'background': True, + 'color_texture': bs.gettexture("rampageBGColor") + }) + + self.leftwall = bs.newnode('region', attrs={'position': (-17.75152479, 13, -5.52), 'scale': ( + 0.1, 15.5, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.rightwall = bs.newnode('region', attrs={'position': (17.75, 13, -5.52), 'scale': ( + 0.1, 15.5, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + self.topwall = bs.newnode('region', attrs={'position': (0, 21.0, -5.52), 'scale': ( + 35.4, 0.2, 2), 'type': 'box', 'materials': [shared.footing_material, self._real_wall_material]}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (-17.75152479, 13, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.1, 15.5, 2)}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (17.75, 13, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (0.1, 15.5, 2)}) + bs.newnode('locator', attrs={'shape': 'box', 'position': (0, 21.0, -5.52), 'color': ( + 0, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': (35.4, 0.2, 2)}) + + gnode = bs.getactivity().globalsnode + gnode.happy_thoughts_mode = True + gnode.shadow_offset = (0.0, 8.0, 5.0) + gnode.tint = (1.3, 1.23, 1.0) + gnode.ambient_color = (1.3, 1.23, 1.0) + gnode.vignette_outer = (0.64, 0.59, 0.69) + gnode.vignette_inner = (0.95, 0.95, 0.93) + gnode.vr_near_clip = 1.0 + self.is_flying = True + + # throw out some tips on flying + txt = bs.newnode('text', + attrs={ + 'text': babase.Lstr(resource='pressJumpToFlyText'), + 'scale': 1.2, + 'maxwidth': 800, + 'position': (0, 200), + 'shadow': 0.5, + 'flatness': 0.5, + 'h_align': 'center', + 'v_attach': 'bottom' + }) + cmb = bs.newnode('combine', + owner=txt, + attrs={ + 'size': 4, + 'input0': 0.3, + 'input1': 0.9, + 'input2': 0.0 + }) + bs.animate(cmb, 'input3', {3.0: 0, 4.0: 1, 9.0: 1, 10.0: 0}) + cmb.connectattr('output', txt, 'color') + bs.timer(10.0, txt.delete) + + +try: + bs._map.register_map(CreativeThoughts) +except: + pass diff --git a/plugins/minigames/simon_says.py b/plugins/minigames/simon_says.py new file mode 100644 index 000000000..4495d3a76 --- /dev/null +++ b/plugins/minigames/simon_says.py @@ -0,0 +1,388 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# SimonSays +# you had really better do what Simon says... +# ba_meta require api 8 +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Union, Sequence + +from bascenev1 import _gameutils +import babase +import bauiv1 as bui +import bascenev1 as bs +import random + + +class CustomText(bs.Actor): + """Text that pops up above a position to denote something special. + + category: Gameplay Classes + """ + + def __init__(self, + text: Union[str, babase.Lstr], + position: Sequence[float] = (0.0, 0.0, 0.0), + color: Sequence[float] = (1.0, 1.0, 1.0, 1.0), + random_offset: float = 0.5, + duration: float = 1.5, + offset: Sequence[float] = (0.0, 0.0, 0.0), + scale: float = 1.0): + super().__init__() + if len(color) == 3: + color = (color[0], color[1], color[2], 1.0) + pos = (position[0] + offset[0] + random_offset * + (0.5 - random.random()), position[1] + offset[0] + + random_offset * (0.5 - random.random()), position[2] + + offset[0] + random_offset * (0.5 - random.random())) + self.node = bs.newnode('text', + attrs={ + 'text': text, + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_align': 'center'}, delegate=self) + lifespan = duration + bs.animate( + self.node, 'scale', { + 0: 0.0, + lifespan * 0.11: 0.020 * 0.7 * scale, + lifespan * 0.16: 0.013 * 0.7 * scale, + lifespan * 0.25: 0.014 * 0.7 * scale + }) + self._tcombine = bs.newnode('combine', + owner=self.node, + attrs={ + 'input0': pos[0], + 'input2': pos[2], + 'size': 3 + }) + bs.animate(self._tcombine, 'input1', { + 0: pos[1] + 1.5, + lifespan: pos[1] + 2.0 + }) + self._tcombine.connectattr('output', self.node, 'position') + # fade our opacity in/out + self._combine = bs.newnode('combine', + owner=self.node, + attrs={ + 'input0': color[0], + 'input1': color[1], + 'input2': color[2], + 'size': 4 + }) + for i in range(4): + bs.animate( + self._combine, 'input' + str(i), { + 0.13 * lifespan: color[i], + 0.18 * lifespan: 4.0 * color[i], + 0.22 * lifespan: color[i]}) + bs.animate(self._combine, 'input3', { + 0: 0, + 0.1 * lifespan: color[3], + 0.7 * lifespan: color[3], + lifespan: 0}) + self._combine.connectattr('output', self.node, 'color') + self._die_timer = bs.Timer( + lifespan, bs.WeakCall(self.handlemessage, bs.DieMessage())) + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.score = 0 + self.dead: bool = False + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + +# ba_meta export bascenev1.GameActivity + + +class SimonSays(bs.TeamGameActivity[Player, Team]): + name = "Simon Says" + description = "You have to better do what Simon says!" + + @classmethod + def get_available_settings(cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.BoolSetting("Epic Mode", default=False), + bs.BoolSetting("Enable Jumping", default=False), + bs.BoolSetting("Enable Punching", default=False), + bs.BoolSetting("Enable Picking Up", default=False), + bs.IntChoiceSetting("Timer Speed", + choices=[("Snaily", 1200), + ("Slow", 900), + ("Normal", 655), + ("Fast", 544), + ("Turbo", 460)], default=655), + + bs.FloatChoiceSetting("Text Duration", + choices=[("Slow", 2.5), + ("Normal", 1.5), + ("Mediocre", 1.0), + ("Quick", 0.75)], default=1.5)] + return settings + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ["Courtyard"] + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.FreeForAllSession) + + def __init__(self, settings: dict): + super().__init__(settings) + self.settings = settings + self._is_slow_motion = bool(settings['Epic Mode']) + self.speed = float(settings['Timer Speed']) + self.lifespan = float(settings['Text Duration']) + self.round_num = 0 + self.string = "" + self.now = 0 + self.simon = False + self.ended = False + self.counter_loop = None + self.time = 5000 + self._r1 = 2 + self.ct_text = bs.newnode('text', attrs={ + 'in_world': True, + 'text': '......', + 'shadow': 1.0, + 'color': (1.0, 1.0, 1.0), + 'flatness': 0.5, + 'position': (-5.627144702, 3.3275475, -9.572879116), + 'scale': 0.05}) + self.n1 = bs.newnode('locator', attrs={'shape': 'circle', 'position': (-4, 0, -6), + 'color': (1, 0, 0), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n2 = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, 0, -6), + 'color': (0, 1, 0), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n3 = bs.newnode('locator', attrs={'shape': 'circle', 'position': (4, 0, -6), + 'color': (0, 0, 1), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n4 = bs.newnode('locator', attrs={'shape': 'circle', 'position': (-4, 0, -2), + 'color': (1, 1, 0), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n5 = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, 0, -2), + 'color': (0, 1, 1), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n6 = bs.newnode('locator', attrs={'shape': 'circle', 'position': (4, 0, -2), + 'color': (1, 0, 1), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n7 = bs.newnode('locator', attrs={'shape': 'circle', 'position': (-4, 0, 2), + 'color': (.5, .5, .5), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n8 = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, 0, 2), + 'color': (.5, .325, 0), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.n9 = bs.newnode('locator', attrs={'shape': 'circle', 'position': (4, 0, 2), + 'color': (1, 1, 1), 'opacity': 0.5, + 'draw_beauty': True, 'additive': True}) + self.options = ["red", "green", "blue", "yellow", "teal", "purple", "gray", "orange", + "white", "top", "bottom", "middle row", "left", "right", "center column", "outside"] + self.default_music = bs.MusicType.FLAG_CATCHER + + def get_instance_description(self) -> str: + return 'Follow the commands... but only when \"Simon says!"' + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0),) + return + else: + self.spawn_player(player) + + def on_begin(self) -> None: + super().on_begin() + s = self.settings + _gameutils.animate_array(self.n1, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n2, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n3, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n4, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n5, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n6, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n7, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n8, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + _gameutils.animate_array(self.n9, 'size', 1, {0: [0.0], 0.2: [self._r1*2.0]}) + for team in self.teams: + team.score = 0 + for player in self.players: + player.score = 0 + # check for immediate end if theres only 1 player + if len(self.players) == 1: + bs.timer(4000/1000, bs.Call(self.check_end)) + bs.timer(6000/1000, self.call_round) + + def spawn_player(self, player: PlayerT) -> bs.Actor: + assert player + spaz = self.spawn_player_spaz(player, position=( + 0 + random.uniform(-3.6, 3.6), 2.9, -2 + random.uniform(-3.6, 3.6))) + assert spaz.node + spaz.connect_controls_to_player( + enable_bomb=False, + enable_run=True, + enable_punch=self.settings["Enable Punching"], + enable_pickup=self.settings["Enable Picking Up"], + enable_jump=self.settings["Enable Jumping"]) + + def call_round(self) -> None: + if self.ended: + return + self.round_num += 1 + self.num = random.randint(0, 15) + self.numa = self.num + self.simon = random.choice([True, False]) + false_prefix = random.choices(['Simon say r', 'Simon said r', 'Simon r', + 'Simons says r', 'Simons r', 'R'], weights=[35, 45, 45, 39, 49, 100])[0] + if self.numa < 9: + if not self.simon: + line = false_prefix + "un to the " + self.options[self.numa] + " circle!" + else: + line = "Run to the " + self.options[self.numa] + " circle!" + + elif self.numa < 15: + if not self.simon: + line = false_prefix + "un to the " + self.options[self.numa] + "!" + else: + line = "Run to the " + self.options[self.numa] + "!" + + else: + if not self.simon: + line = false_prefix + "un outside of the circles!" + else: + line = "Run outside of the circles!" + + if self.simon: + line = "Simon says " + line[0].lower() + line[1:] + self.text = CustomText(line, + position=(0, 5, -4), + color=(0.68, 0.95, 1.12), + random_offset=0.5, + offset=(0, 0, 0), + duration=self.lifespan, + scale=2.0).autoretain() + self.now = 6 + + def dummy_check(): + self.string = "...." + self.check_round() + + def set_counter(): + self.now = self.now - 1 + if self.now == 0: + self.string = "0" + self.ct_text.text = self.string + self.counter_loop = None + bs.timer(1/1000, dummy_check) + else: + self.ct_text.text = str(self.now) + bs.getsound('tick').play() + self.counter_loop = bs.Timer(self.speed/1000, set_counter, repeat=True) + + def check_round(self) -> None: + if self.ended: + return + for player in self.players: + if player.is_alive(): + safe = True if self.options[self.numa] in self.in_circle( + player.actor.node.position_center) else False + if ((self.simon and safe == False) or ((not self.simon) and safe == True)): + player.team.score = self.round_num + player.actor.handlemessage(bs.DieMessage()) + bs.timer(1633/1000, self.call_round) + + def in_circle(self, pos) -> None: + circles = [] + x = pos[0] + z = pos[2] + if (x + 4) ** 2 + (z + 6) ** 2 < 4: + circles.append("red") + elif (x) ** 2 + (z + 6) ** 2 < 4: + circles.append("green") + elif (x - 4) ** 2 + (z + 6) ** 2 < 4: + circles.append("blue") + elif (x + 4) ** 2 + (z + 2) ** 2 < 4: + circles.append("yellow") + elif (x) ** 2 + (z + 2) ** 2 < 4: + circles.append("teal") + elif (x - 4) ** 2 + (z + 2) ** 2 < 4: + circles.append("purple") + elif (x + 4) ** 2 + (z - 2) ** 2 < 4: + circles.append("gray") + elif (x) ** 2 + (z - 2) ** 2 < 4: + circles.append("orange") + elif (x - 4) ** 2 + (z - 2) ** 2 < 4: + circles.append("white") + else: + circles.append("outside") + if x < -2: + circles.append("left") + if x > 2: + circles.append("right") + if x > -2 and x < 2: + circles.append("center column") + if z > 0: + circles.append("bottom") + if z < -4: + circles.append("top") + if z < 0 and z > -4: + circles.append("middle row") + return circles + + def handlemessage(self, msg) -> None: + if isinstance(msg, bs.PlayerDiedMessage): + msg.getplayer(Player).dead = True + self.check_end() + else: + super().handlemessage(msg) + + def end_game(self) -> None: + self.ended = True + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def check_end(self): + i = 0 + for player in self.players: + if player.is_alive() and not player.dead: + i += 1 + if isinstance(self.session, bs.CoopSession): + if i <= 0: + bs.timer(0.6, bs.Call(self.end_game)) + else: + if i <= 2: + bs.timer(0.6, bs.Call(self.end_game)) + + +# ba_meta export plugin +class plugin(babase.Plugin): + def __init__(self): + ## Campaign support ## + babase.app.classic.add_coop_practice_level(bs.Level( + name='Simon Says', + gametype=SimonSays, + settings={}, + preview_texture_name='courtyardPreview')) diff --git a/plugins/minigames/sleep_race.py b/plugins/minigames/sleep_race.py new file mode 100644 index 000000000..21ad95af1 --- /dev/null +++ b/plugins/minigames/sleep_race.py @@ -0,0 +1,782 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# y me (: itsre3 +# =>2<= +# BCS RULES +# +"""Defines Race mini-game.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +from dataclasses import dataclass + +import babase +import bascenev1 as bs +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict, + Union) + from bascenev1lib.actor.onscreentimer import OnScreenTimer + + +@dataclass +class RaceMine: + """Holds info about a mine on the track.""" + point: Sequence[float] + mine: Optional[Bomb] + + +class RaceRegion(bs.Actor): + """Region used to track progress during a race.""" + + def __init__(self, pt: Sequence[float], index: int): + super().__init__() + activity = self.activity + assert isinstance(activity, RaceGame) + self.pos = pt + self.index = index + self.node = bs.newnode( + 'region', + delegate=self, + attrs={ + 'position': pt[:3], + 'scale': (pt[3] * 2.0, pt[4] * 2.0, pt[5] * 2.0), + 'type': 'box', + 'materials': [activity.race_region_material] + }) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.distance_txt: Optional[bs.Node] = None + self.last_region = 0 + self.lap = 0 + self.distance = 0.0 + self.finished = False + self.rank: Optional[int] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.time: Optional[float] = None + self.lap = 0 + self.finished = False + + +# ba_meta export bascenev1.GameActivity +class SleepRaceGame(bs.TeamGameActivity[Player, Team]): + """Game of racing around a track.""" + + name = 'Sleep Race' + description = 'Can you run while sleeping?' + scoreconfig = bs.ScoreConfig(label='Time', + lower_is_better=True, + scoretype=bs.ScoreType.MILLISECONDS) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting('Laps', min_value=1, default=2, increment=1), + bs.IntChoiceSetting( + 'Time Limit', + default=0, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + ), + bs.IntChoiceSetting( + 'Mine Spawning', + default=4000, + choices=[ + ('No Mines', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ], + ), + bs.IntChoiceSetting( + 'Bomb Spawning', + choices=[ + ('None', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ('1 Second', 1000), + ], + default=2000, + ), + bs.IntChoiceSetting( + 'Knockout Time', + choices=[ + ('8 Seconds', 8000), + ('5 Seconds', 5000), + ], + default=5000, + ), + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Credits', default=True), + ] + + # We have some specific settings in teams mode. + if issubclass(sessiontype, bs.DualTeamSession): + settings.append( + bs.BoolSetting('Entire Team Must Finish', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.MultiTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('race') + + def __init__(self, settings: dict): + self._race_started = False + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_sound = bs.getsound('score') + self._swipsound = bs.getsound('swip') + self._last_team_time: Optional[float] = None + self._front_race_region: Optional[int] = None + self._nub_tex = bs.gettexture('nub') + self._beep_1_sound = bs.getsound('raceBeep1') + self._beep_2_sound = bs.getsound('raceBeep2') + self.race_region_material: Optional[bs.Material] = None + self._regions: List[RaceRegion] = [] + self._team_finish_pts: Optional[int] = None + self._time_text: Optional[bs.Actor] = None + self._cd_text: Optional[bs.Actor] = None + self._timer: Optional[OnScreenTimer] = None + self._race_mines: Optional[List[RaceMine]] = None + self._race_mine_timer: Optional[bs.Timer] = None + self._scoreboard_timer: Optional[bs.Timer] = None + self._player_order_update_timer: Optional[bs.Timer] = None + self._start_lights: Optional[List[bs.Node]] = None + self._bomb_spawn_timer: Optional[bs.Timer] = None + self._knockout_timer: Optional[bs.Timer] = None + self._laps = int(settings['Laps']) + self._entire_team_must_finish = bool( + settings.get('Entire Team Must Finish', False)) + self._time_limit = float(settings['Time Limit']) + self._mine_spawning = int(settings['Mine Spawning']) + self._bomb_spawning = int(settings['Bomb Spawning']) + self._knockout_time = float(settings['Knockout Time']) + self._epic_mode = bool(settings['Epic Mode']) + self._credits = bool(settings['Credits']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC_RACE + if self._epic_mode else bs.MusicType.RACE) + + def get_instance_description(self) -> Union[str, Sequence]: + if (isinstance(self.session, bs.DualTeamSession) + and self._entire_team_must_finish): + t_str = ' Your entire team has to finish.' + else: + t_str = '' + + if self._laps > 1: + return 'Run ${ARG1} laps.' + t_str, self._laps + return 'Run 1 lap.' + t_str + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._laps > 1: + return 'run ${ARG1} laps', self._laps + return 'run 1 lap' + + def on_transition_in(self) -> None: + super().on_transition_in() + shared = SharedObjects.get() + pts = self.map.get_def_points('race_point') + mat = self.race_region_material = bs.Material() + mat.add_actions(conditions=('they_have_material', + shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + self._handle_race_point_collide), + )) + for rpt in pts: + self._regions.append(RaceRegion(rpt, len(self._regions))) + + def _flash_player(self, player: Player, scale: float) -> None: + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + pos = player.actor.node.position + light = bs.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + bs.timer(0.5, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) + + def _handle_race_point_collide(self) -> None: + # FIXME: Tidy this up. + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + collision = bs.getcollision() + try: + region = collision.sourcenode.getdelegate(RaceRegion, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + last_region = player.last_region + this_region = region.index + + if last_region != this_region: + + # If a player tries to skip regions, smite them. + # Allow a one region leeway though (its plausible players can get + # blown over a region, etc). + if this_region > last_region + 2: + if player.is_alive(): + assert player.actor + player.actor.handlemessage(bs.DieMessage()) + bs.broadcastmessage(babase.Lstr( + translate=('statements', 'Killing ${NAME} for' + ' skipping part of the track!'), + subs=[('${NAME}', player.getname(full=True))]), + color=(1, 0, 0)) + else: + # If this player is in first, note that this is the + # front-most race-point. + if player.rank == 0: + self._front_race_region = this_region + + player.last_region = this_region + if last_region >= len(self._regions) - 2 and this_region == 0: + team = player.team + player.lap = min(self._laps, player.lap + 1) + + # In teams mode with all-must-finish on, the team lap + # value is the min of all team players. + # Otherwise its the max. + if isinstance(self.session, bs.DualTeamSession + ) and self._entire_team_must_finish: + team.lap = min([p.lap for p in team.players]) + else: + team.lap = max([p.lap for p in team.players]) + + # A player is finishing. + if player.lap == self._laps: + + # In teams mode, hand out points based on the order + # players come in. + if isinstance(self.session, bs.DualTeamSession): + assert self._team_finish_pts is not None + if self._team_finish_pts > 0: + self.stats.player_scored(player, + self._team_finish_pts, + screenmessage=False) + self._team_finish_pts -= 25 + + # Flash where the player is. + self._flash_player(player, 1.0) + player.finished = True + assert player.actor + player.actor.handlemessage( + bs.DieMessage(immediate=True)) + + # Makes sure noone behind them passes them in rank + # while finishing. + player.distance = 9999.0 + + # If the whole team has finished the race. + if team.lap == self._laps: + self._score_sound.play() + player.team.finished = True + assert self._timer is not None + elapsed = bs.time() - self._timer.getstarttime() + self._last_team_time = player.team.time = elapsed + self._check_end_game() + + # Team has yet to finish. + else: + self._swipsound.play() + + # They've just finished a lap but not the race. + else: + self._swipsound.play() + self._flash_player(player, 0.3) + + # Print their lap number over their head. + try: + assert isinstance(player.actor, PlayerSpaz) + mathnode = bs.newnode('math', + owner=player.actor.node, + attrs={ + 'input1': (0, 1.9, 0), + 'operation': 'add' + }) + player.actor.node.connectattr( + 'torso_position', mathnode, 'input2') + tstr = babase.Lstr(resource='lapNumberText', + subs=[('${CURRENT}', + str(player.lap + 1)), + ('${TOTAL}', str(self._laps)) + ]) + txtnode = bs.newnode('text', + owner=mathnode, + attrs={ + 'text': tstr, + 'in_world': True, + 'color': (1, 1, 0, 1), + 'scale': 0.015, + 'h_align': 'center' + }) + mathnode.connectattr('output', txtnode, 'position') + bs.animate(txtnode, 'scale', { + 0.0: 0, + 0.2: 0.019, + 2.0: 0.019, + 2.2: 0 + }) + bs.timer(2.3, mathnode.delete) + except Exception: + babase.print_exception('Error printing lap.') + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + + # A player leaving disqualifies the team if 'Entire Team Must Finish' + # is on (otherwise in teams mode everyone could just leave except the + # leading player to win). + if (isinstance(self.session, bs.DualTeamSession) + and self._entire_team_must_finish): + bs.broadcastmessage(babase.Lstr( + translate=('statements', + '${TEAM} is disqualified because ${PLAYER} left'), + subs=[('${TEAM}', player.team.name), + ('${PLAYER}', player.getname(full=True))]), + color=(1, 1, 0)) + player.team.finished = True + player.team.time = None + player.team.lap = 0 + bs.getsound('boo').play() + for otherplayer in player.team.players: + otherplayer.lap = 0 + otherplayer.finished = True + try: + if otherplayer.actor is not None: + otherplayer.actor.handlemessage(bs.DieMessage()) + except Exception: + babase.print_exception('Error sending DieMessage.') + + # Defer so team/player lists will be updated. + babase.pushcall(self._check_end_game) + + def _update_scoreboard(self) -> None: + for team in self.teams: + distances = [player.distance for player in team.players] + if not distances: + teams_dist = 0.0 + else: + if (isinstance(self.session, bs.DualTeamSession) + and self._entire_team_must_finish): + teams_dist = min(distances) + else: + teams_dist = max(distances) + self._scoreboard.set_team_value( + team, + teams_dist, + self._laps, + flash=(teams_dist >= float(self._laps)), + show_value=False) + + def on_begin(self) -> None: + from bascenev1lib.actor.onscreentimer import OnScreenTimer + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._team_finish_pts = 100 + if self._credits: + self._cd_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'position': (0, 0), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (1, 1, 1), + 'text': 'By itsre3' + })) + + # Throw a timer up on-screen. + self._time_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.5, 1), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, -50), + 'scale': 1.4, + 'text': '' + })) + self._timer = OnScreenTimer() + + if self._mine_spawning != 0: + self._race_mines = [ + RaceMine(point=p, mine=None) + for p in self.map.get_def_points('race_mine') + ] + if self._race_mines: + self._race_mine_timer = bs.Timer(0.001 * self._mine_spawning, + self._update_race_mine, + repeat=True) + + self._scoreboard_timer = bs.Timer(0.25, + self._update_scoreboard, + repeat=True) + self._player_order_update_timer = bs.Timer(0.25, + self._update_player_order, + repeat=True) + + if self.slow_motion: + t_scale = 0.4 + light_y = 50 + else: + t_scale = 1.0 + light_y = 150 + lstart = 7.1 * t_scale + inc = 1.25 * t_scale + + bs.timer(lstart, self._do_light_1) + bs.timer(lstart + inc, self._do_light_2) + bs.timer(lstart + 2 * inc, self._do_light_3) + bs.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = bs.newnode('image', + attrs={ + 'texture': bs.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + bs.animate( + lnub, 'opacity', { + 4.0 * t_scale: 0, + 5.0 * t_scale: 1.0, + 12.0 * t_scale: 1.0, + 12.5 * t_scale: 0.0 + }) + bs.timer(13.0 * t_scale, lnub.delete) + self._start_lights.append(lnub) + + self._start_lights[0].color = (0.2, 0, 0) + self._start_lights[1].color = (0.2, 0, 0) + self._start_lights[2].color = (0.2, 0.05, 0) + self._start_lights[3].color = (0.0, 0.3, 0) + + def _do_light_1(self) -> None: + assert self._start_lights is not None + self._start_lights[0].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + self._beep_1_sound.play() + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + self._beep_2_sound.play() + for player in self.players: + if player.actor is not None: + try: + assert isinstance(player.actor, PlayerSpaz) + player.actor.connect_controls_to_player() + except Exception: + babase.print_exception('Error in race player connects.') + assert self._timer is not None + self._timer.start() + + if self._bomb_spawning != 0: + self._bomb_spawn_timer = bs.Timer(0.001 * self._bomb_spawning, + self._spawn_bomb, + repeat=True) + + def knock_players(): + activity = bs.get_foreground_host_activity() + gnode = bs.getactivity().globalsnode + for players in activity.players: + gnode.tint = (0.5, 0.5, 0.5) + node = players.actor.node + node.handlemessage('knockout', 600.0) + self.text_offset = bs.newnode('math', + owner=node, + attrs={'input1': (-0.5, 0.5, 0.25), + 'operation': 'add'}) + node.connectattr( + 'torso_position', + self.text_offset, + 'input2') + self.text = bs.newnode('text', + owner=node, + attrs={ + 'h_align': 'right', + 'color': (1.0, 1.0, 1.0), + 'shadow': 1.0, + 'text': 'z z', + 'scale': 0.01, + 'in_world': True}) + self.text_offset.connectattr( + 'output', + self.text, + 'position') + bs.animate(self.text, 'scale', {0: 0.0, 1.0: 0.01}) + bs.timer(2, self.text.delete) + + if self._knockout_time != 0: + knock_time = 0.001 * self._knockout_time + self._knockout_timer = bs.Timer(knock_time, + knock_players, + repeat=True) + + self._race_started = True + + def _update_player_order(self) -> None: + + # Calc all player distances. + for player in self.players: + pos: Optional[babase.Vec3] + try: + pos = player.position + except bs.NotFoundError: + pos = None + if pos is not None: + r_index = player.last_region + rg1 = self._regions[r_index] + r1pt = babase.Vec3(rg1.pos[:3]) + rg2 = self._regions[0] if r_index == len( + self._regions) - 1 else self._regions[r_index + 1] + r2pt = babase.Vec3(rg2.pos[:3]) + r2dist = (pos - r2pt).length() + amt = 1.0 - (r2dist / (r2pt - r1pt).length()) + amt = player.lap + (r_index + amt) * (1.0 / len(self._regions)) + player.distance = amt + + # Sort players by distance and update their ranks. + p_list = [(player.distance, player) for player in self.players] + + p_list.sort(reverse=True, key=lambda x: x[0]) + for i, plr in enumerate(p_list): + plr[1].rank = i + if plr[1].actor: + node = plr[1].distance_txt + if node: + node.text = str(i + 1) if plr[1].is_alive() else '' + + def _spawn_bomb(self) -> None: + if self._front_race_region is None: + return + region = (self._front_race_region + 3) % len(self._regions) + pos = self._regions[region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1] + 1.0, + pos[2] + random.uniform(*z_range)) + bs.timer(random.uniform(0.0, 2.0), + bs.WeakCall(self._spawn_bomb_at_pos, pos)) + + def _spawn_bomb_at_pos(self, pos: Sequence[float]) -> None: + if self.has_ended(): + return + Bomb(position=pos, bomb_type='normal').autoretain() + + def _make_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + rmine.mine = Bomb(position=rmine.point[:3], bomb_type='land_mine') + rmine.mine.arm() + + def _flash_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + light = bs.newnode('light', + attrs={ + 'position': rmine.point[:3], + 'color': (1, 0.2, 0.2), + 'radius': 0.1, + 'height_attenuated': False + }) + bs.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) + bs.timer(1.0, light.delete) + + def _update_race_mine(self) -> None: + assert self._race_mines is not None + m_index = -1 + rmine = None + for _i in range(3): + m_index = random.randrange(len(self._race_mines)) + rmine = self._race_mines[m_index] + if not rmine.mine: + break + assert rmine is not None + if not rmine.mine: + self._flash_mine(m_index) + bs.timer(0.95, babase.Call(self._make_mine, m_index)) + + def spawn_player(self, player: Player) -> bs.Actor: + if player.team.finished: + # FIXME: This is not type-safe! + # This call is expected to always return an Actor! + # Perhaps we need something like can_spawn_player()... + # noinspection PyTypeChecker + return None # type: ignore + pos = self._regions[player.last_region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1], + pos[2] + random.uniform(*z_range)) + spaz = self.spawn_player_spaz( + player, position=pos, angle=90 if not self._race_started else None) + assert spaz.node + # Prevent controlling of characters before the start of the race. + if not self._race_started: + spaz.disconnect_controls_from_player() + mathnode = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + spaz.node.connectattr('torso_position', mathnode, 'input2') + + distance_txt = bs.newnode('text', + owner=spaz.node, + attrs={ + 'text': '', + 'in_world': True, + 'color': (1, 1, 0.4), + 'scale': 0.02, + 'h_align': 'center' + }) + player.distance_txt = distance_txt + mathnode.connectattr('output', distance_txt, 'position') + + def _check_end_game(self) -> None: + + # If there's no teams left racing, finish. + teams_still_in = len([t for t in self.teams if not t.finished]) + if teams_still_in == 0: + self.end_game() + return + + # Count the number of teams that have completed the race. + teams_completed = len( + [t for t in self.teams if t.finished and t.time is not None]) + + if teams_completed > 0: + session = self.session + + # In teams mode its over as soon as any team finishes the race + + # FIXME: The get_ffa_point_awards code looks dangerous. + if isinstance(session, bs.DualTeamSession): + self.end_game() + else: + # In ffa we keep the race going while there's still any points + # to be handed out. Find out how many points we have to award + # and how many teams have finished, and once that matches + # we're done. + assert isinstance(session, bs.FreeForAllSession) + points_to_award = len(session.get_ffa_point_awards()) + if teams_completed >= points_to_award - teams_completed: + self.end_game() + return + + def end_game(self) -> None: + + # Stop updating our time text, and set it to show the exact last + # finish time if we have one. (so users don't get upset if their + # final time differs from what they see onscreen by a tiny amount) + assert self._timer is not None + if self._timer.has_started(): + self._timer.stop( + endtime=None if self._last_team_time is None else ( + self._timer.getstarttime() + self._last_team_time)) + + results = bs.GameResults() + + for team in self.teams: + if team.time is not None: + # We store time in seconds, but pass a score in milliseconds. + results.set_team_score(team, int(team.time * 1000.0)) + else: + results.set_team_score(team, None) + + # We don't announce a winner in ffa mode since its probably been a + # while since the first place guy crossed the finish line so it seems + # odd to be announcing that now. + self.end(results=results, + announce_winning_team=isinstance(self.session, + bs.DualTeamSession)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # Augment default behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + if not player.finished: + self.respawn_player(player, respawn_time=1) + else: + super().handlemessage(msg) diff --git a/plugins/minigames/snake.py b/plugins/minigames/snake.py new file mode 100644 index 000000000..454a07036 --- /dev/null +++ b/plugins/minigames/snake.py @@ -0,0 +1,324 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# snake +# Released under the MIT License. See LICENSE for details. +# +"""Snake game by SEBASTIAN2059""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor import bomb as stdbomb + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + + +class ScoreMessage: + """It will help us with the scores.""" + + def __init__(self, player: Player): + self.player = player + + def getplayer(self): + return self.player + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + + self.mines = [] + self.actived = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +lang = bs.app.lang.language +if lang == 'Spanish': + description = 'Sobrevive a un número determinado de minas para ganar.' + join_description = 'Corre y no te dejes matar.' + view_description = 'sobrevive ${ARG1} minas' + +else: + description = 'Survive a set number of mines to win.' + join_description = "Run and don't get killed." + view_description = 'survive ${ARG1} mines' + + +class Custom_Mine(stdbomb.Bomb): + """Custom a mine :)""" + + def __init__(self, position, source_player): + stdbomb.Bomb.__init__(self, position=position, bomb_type='land_mine', + source_player=source_player) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.HitMessage): + return + else: + super().handlemessage(msg) + +# ba_meta export bascenev1.GameActivity + + +class SnakeGame(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Snake' + description = description + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + 'Score to Win', + min_value=40, + default=80, + increment=5, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = bs.getsound('dingSmall') + + self._beep_1_sound = bs.getsound('raceBeep1') + self._beep_2_sound = bs.getsound('raceBeep2') + + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + self._started = False + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return join_description + + def get_instance_description_short(self) -> Union[str, Sequence]: + return view_description, self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + if self.slow_motion: + t_scale = 0.4 + light_y = 50 + else: + t_scale = 1.0 + light_y = 150 + lstart = 7.1 * t_scale + inc = 1.25 * t_scale + + bs.timer(lstart, self._do_light_1) + bs.timer(lstart + inc, self._do_light_2) + bs.timer(lstart + 2 * inc, self._do_light_3) + bs.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = bs.newnode('image', + attrs={ + 'texture': bs.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + bs.animate( + lnub, 'opacity', { + 4.0 * t_scale: 0, + 5.0 * t_scale: 1.0, + 12.0 * t_scale: 1.0, + 12.5 * t_scale: 0.0 + }) + bs.timer(13.0 * t_scale, lnub.delete) + self._start_lights.append(lnub) + + self._start_lights[0].color = (0.2, 0, 0) + self._start_lights[1].color = (0.2, 0, 0) + self._start_lights[2].color = (0.2, 0.05, 0) + self._start_lights[3].color = (0.0, 0.3, 0) + + def _do_light_1(self) -> None: + assert self._start_lights is not None + self._start_lights[0].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + self._beep_1_sound.play() + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + self._beep_2_sound.play() + + self._started = True + + for player in self.players: + self.generate_mines(player) + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> bs.Actor: + spaz = self.spawn_player_spaz(player) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_pickup=False) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + if self._started: + self.generate_mines(player) + return spaz + + def generate_mines(self, player: Player): + try: + player.actived = bs.Timer(0.5, babase.Call(self.spawn_mine, player), repeat=True) + except Exception as e: + print('Exception -> ' + str(e)) + + def spawn_mine(self, player: Player): + if player.team.score >= self._score_to_win: + return + pos = player.actor.node.position + # mine = stdbomb.Bomb(position=(pos[0], pos[1] + 2.0, pos[2]), + # velocity=(0, 0, 0), + # bomb_type='land_mine', + # #blast_radius=, + # source_player=player.actor.source_player, + # owner=player.actor.node).autoretain() + mine = Custom_Mine(position=(pos[0], pos[1] + 2.0, pos[2]), + source_player=player.actor.source_player) + + def arm(): + mine.arm() + bs.timer(0.5, arm) + + player.mines.append(mine) + if len(player.mines) > 15: + for m in player.mines: + try: + m.node.delete() + except Exception: + pass + player.mines.remove(m) + break + + self.handlemessage(ScoreMessage(player)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + player.actived = None + + elif isinstance(msg, ScoreMessage): + player = msg.getplayer() + + player.team.score += 1 + self._update_scoreboard() + + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + self.end_game() # bs.timer(0.5, self.end_game) + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/plugins/minigames/snow_ball_fight.py b/plugins/minigames/snow_ball_fight.py new file mode 100644 index 000000000..0bffe2dbd --- /dev/null +++ b/plugins/minigames/snow_ball_fight.py @@ -0,0 +1,643 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor.bomb import Blast +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.spaz import PunchHitMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.spazfactory import SpazFactory + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = bs.app.lang.language + +if lang == 'Spanish': + name = 'Guerra de Nieve' + snowball_rate = 'Intervalo de Ataque' + snowball_slowest = 'Más Lento' + snowball_slow = 'Lento' + snowball_fast = 'Rápido' + snowball_lagcity = 'Más Rápido' + snowball_scale = 'Tamaño de Bola de Nieve' + snowball_smallest = 'Más Pequeño' + snowball_small = 'Pequeño' + snowball_big = 'Grande' + snowball_biggest = 'Más Grande' + snowball_insane = 'Insano' + snowball_melt = 'Derretir Bola de Nieve' + snowball_bust = 'Rebotar Bola de Nieve' + snowball_explode = 'Explotar al Impactar' + snowball_snow = 'Modo Nieve' +else: + name = 'Snowball Fight' + snowball_rate = 'Snowball Rate' + snowball_slowest = 'Slowest' + snowball_slow = 'Slow' + snowball_fast = 'Fast' + snowball_lagcity = 'Lag City' + snowball_scale = 'Snowball Scale' + snowball_smallest = 'Smallest' + snowball_small = 'Small' + snowball_big = 'Big' + snowball_biggest = 'Biggest' + snowball_insane = 'Insane' + snowball_melt = 'Snowballs Melt' + snowball_bust = 'Snowballs Bust' + snowball_explode = 'Snowballs Explode' + snowball_snow = 'Snow Mode' + + +class Snowball(bs.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + blast_radius: float = 0.7, + bomb_scale: float = 0.8, + source_player: bs.Player | None = None, + owner: bs.Node | None = None, + melt: bool = True, + bounce: bool = True, + explode: bool = False): + super().__init__() + shared = SharedObjects.get() + self._exploded = False + self.scale = bomb_scale + self.blast_radius = blast_radius + self._source_player = source_player + self.owner = owner + self._hit_nodes = set() + self.snowball_melt = melt + self.snowball_bounce = bounce + self.snowball_explode = explode + self.radius = bomb_scale * 0.1 + if bomb_scale <= 1.0: + shadow_size = 0.6 + elif bomb_scale <= 2.0: + shadow_size = 0.4 + elif bomb_scale <= 3.0: + shadow_size = 0.2 + else: + shadow_size = 0.1 + + self.snowball_material = bs.Material() + self.snowball_material.add_actions( + conditions=( + ( + ('we_are_younger_than', 5), + 'or', + ('they_are_younger_than', 100), + ), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + + self.snowball_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=('modify_part_collision', 'use_node_collide', False), + ) + + self.snowball_material.add_actions(actions=('modify_part_collision', + 'friction', 0.3)) + + self.snowball_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('modify_part_collision', 'physical', False), + ('call', 'at_connect', self.hit))) + + self.snowball_material.add_actions( + conditions=(('they_dont_have_material', shared.player_material), + 'and', + ('they_have_material', shared.object_material), + 'or', + ('they_have_material', shared.footing_material)), + actions=('call', 'at_connect', self.bounce)) + + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'body': 'sphere', + 'body_scale': self.scale, + 'mesh': bs.getmesh('frostyPelvis'), + 'shadow_size': shadow_size, + 'color_texture': bs.gettexture('bunnyColor'), + 'reflection': 'soft', + 'reflection_scale': [0.15], + 'density': 1.0, + 'materials': [self.snowball_material] + }) + self.light = bs.newnode( + 'light', + owner=self.node, + attrs={ + 'color': (0.6, 0.6, 1.0), + 'intensity': 0.8, + 'radius': self.radius + }) + self.node.connectattr('position', self.light, 'position') + bs.animate(self.node, 'mesh_scale', { + 0: 0, + 0.2: 1.3 * self.scale, + 0.26: self.scale + }) + bs.animate(self.light, 'radius', { + 0: 0, + 0.2: 1.3 * self.radius, + 0.26: self.radius + }) + if self.snowball_melt: + bs.timer(1.5, bs.WeakCall(self._disappear)) + + def hit(self) -> None: + if not self.node: + return + if self._exploded: + return + if self.snowball_explode: + self._exploded = True + self.do_explode() + bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage())) + else: + self.do_hit() + + def do_hit(self) -> None: + v = self.node.velocity + if babase.Vec3(*v).length() > 5.0: + node = bs.getcollision().opposingnode + if node is not None and node and not ( + node in self._hit_nodes): + t = self.node.position + hitdir = self.node.velocity + self._hit_nodes.add(node) + node.handlemessage( + bs.HitMessage( + pos=t, + velocity=v, + magnitude=babase.Vec3(*v).length()*0.5, + velocity_magnitude=babase.Vec3(*v).length()*0.5, + radius=0, + srcnode=self.node, + source_player=self._source_player, + force_direction=hitdir, + hit_type='snoBall', + hit_subtype='default')) + + if not self.snowball_bounce: + bs.timer(0.05, bs.WeakCall(self.do_bounce)) + + def do_explode(self) -> None: + Blast(position=self.node.position, + velocity=self.node.velocity, + blast_radius=self.blast_radius, + source_player=babase.existing(self._source_player), + blast_type='impact', + hit_subtype='explode').autoretain() + + def bounce(self) -> None: + if not self.node: + return + if self._exploded: + return + if not self.snowball_bounce: + vel = self.node.velocity + bs.timer(0.01, bs.WeakCall(self.calc_bounce, vel)) + else: + return + + def calc_bounce(self, vel) -> None: + if not self.node: + return + ospd = babase.Vec3(*vel).length() + dot = sum(x*y for x, y in zip(vel, self.node.velocity)) + if ospd*ospd - dot > 50.0: + bs.timer(0.05, bs.WeakCall(self.do_bounce)) + + def do_bounce(self) -> None: + if not self.node: + return + if not self._exploded: + self.do_effect() + + def do_effect(self) -> None: + self._exploded = True + bs.emitfx(position=self.node.position, + velocity=[v*0.1 for v in self.node.velocity], + count=10, + spread=0.1, + scale=0.4, + chunk_type='ice') + sound = bs.getsound('impactMedium') + sound.play(1.0, position=self.node.position) + scl = self.node.mesh_scale + bs.animate(self.node, 'mesh_scale', { + 0.0: scl*1.0, + 0.02: scl*0.5, + 0.05: 0.0 + }) + lr = self.light.radius + bs.animate(self.light, 'radius', { + 0.0: lr*1.0, + 0.02: lr*0.5, + 0.05: 0.0 + }) + bs.timer(0.08, + bs.WeakCall(self.handlemessage, bs.DieMessage())) + + def _disappear(self) -> None: + self._exploded = True + if self.node: + scl = self.node.mesh_scale + bs.animate(self.node, 'mesh_scale', { + 0.0: scl*1.0, + 0.3: scl*0.5, + 0.5: 0.0 + }) + lr = self.light.radius + bs.animate(self.light, 'radius', { + 0.0: lr*1.0, + 0.3: lr*0.5, + 0.5: 0.0 + }) + bs.timer(0.55, + bs.WeakCall(self.handlemessage, bs.DieMessage())) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + else: + super().handlemessage(msg) + + +class NewPlayerSpaz(PlayerSpaz): + + def __init__(self, *args: Any, **kwds: Any): + super().__init__(*args, **kwds) + self.snowball_scale = 1.0 + self.snowball_melt = True + self.snowball_bounce = True + self.snowball_explode = False + + def on_punch_press(self) -> None: + if not self.node or self.frozen or self.node.knockout > 0.0: + return + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + if t_ms - self.last_punch_time_ms >= self._punch_cooldown: + if self.punch_callback is not None: + self.punch_callback(self) + + # snowball + pos = self.node.position + p1 = self.node.position_center + p2 = self.node.position_forward + direction = [p1[0]-p2[0], p2[1]-p1[1], p1[2]-p2[2]] + direction[1] = 0.03 + mag = 20.0/babase.Vec3(*direction).length() + vel = [v * mag for v in direction] + Snowball(position=(pos[0], pos[1] + 0.1, pos[2]), + velocity=vel, + blast_radius=self.blast_radius, + bomb_scale=self.snowball_scale, + source_player=self.source_player, + owner=self.node, + melt=self.snowball_melt, + bounce=self.snowball_bounce, + explode=self.snowball_explode).autoretain() + + self._punched_nodes = set() # Reset this. + self.last_punch_time_ms = t_ms + self.node.punch_pressed = True + if not self.node.hold_node: + bs.timer( + 0.1, + bs.WeakCall(self._safe_play_sound, + SpazFactory.get().swish_sound, 0.8)) + self._turbo_filter_add_press('punch') + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, PunchHitMessage): + pass + else: + return super().handlemessage(msg) + return None + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class SnowballFightGame(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = name + description = 'Kill a set number of enemies to win.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.IntChoiceSetting( + snowball_rate, + choices=[ + (snowball_slowest, 500), + (snowball_slow, 400), + ('Normal', 300), + (snowball_fast, 200), + (snowball_lagcity, 100), + ], + default=300, + ), + bs.FloatChoiceSetting( + snowball_scale, + choices=[ + (snowball_smallest, 0.4), + (snowball_small, 0.6), + ('Normal', 0.8), + (snowball_big, 1.4), + (snowball_biggest, 3.0), + (snowball_insane, 6.0), + ], + default=0.8, + ), + bs.BoolSetting(snowball_melt, default=True), + bs.BoolSetting(snowball_bust, default=True), + bs.BoolSetting(snowball_explode, default=False), + bs.BoolSetting(snowball_snow, default=True), + bs.BoolSetting('Epic Mode', default=False), + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: int | None = None + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + self._snowball_rate = int(settings[snowball_rate]) + self._snowball_scale = float(settings[snowball_scale]) + self._snowball_melt = bool(settings[snowball_melt]) + self._snowball_bounce = bool(settings[snowball_bust]) + self._snowball_explode = bool(settings[snowball_explode]) + self._snow_mode = bool(settings[snowball_snow]) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> str | Sequence: + return 'Crush ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> str | Sequence: + return 'kill ${ARG1} enemies', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_transition_in(self) -> None: + super().on_transition_in() + if self._snow_mode: + gnode = bs.getactivity().globalsnode + gnode.tint = (0.8, 0.8, 1.3) + bs.timer(0.02, self.emit_snowball, repeat=True) + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + def emit_snowball(self) -> None: + pos = (-10 + (random.random() * 30), 15, + -10 + (random.random() * 30)) + vel = ((-5.0 + random.random() * 30.0) * (-1.0 if pos[0] > 0 else 1.0), + -50.0, (-5.0 + random.random() * 30.0) * ( + -1.0 if pos[0] > 0 else 1.0)) + bs.emitfx(position=pos, + velocity=vel, + count=10, + scale=1.0 + random.random(), + spread=0.0, + chunk_type='spark') + + def spawn_player_spaz(self, + player: Player, + position: Sequence[float] = (0, 0, 0), + angle: float | None = None) -> PlayerSpaz: + from babase import _math + from bascenev1._gameutils import animate + from bascenev1._coopsession import CoopSession + + if isinstance(self.session, bs.DualTeamSession): + position = self.map.get_start_position(player.team.id) + else: + # otherwise do free-for-all spawn locations + position = self.map.get_ffa_start_position(self.players) + + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = babase.safecolor(color, target_intensity=0.75) + + spaz = NewPlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + # If this is co-op and we're on Courtyard or Runaround, add the + # material that allows us to collide with the player-walls. + # FIXME: Need to generalize this. + if isinstance(self.session, CoopSession) and self.map.getname() in [ + 'Courtyard', 'Tower D' + ]: + mat = self.map.preloaddata['collide_with_wall_material'] + assert isinstance(spaz.node.materials, tuple) + assert isinstance(spaz.node.roller_materials, tuple) + spaz.node.materials += (mat, ) + spaz.node.roller_materials += (mat, ) + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player( + enable_pickup=False, enable_bomb=False) + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + # custom + spaz._punch_cooldown = self._snowball_rate + spaz.snowball_scale = self._snowball_scale + spaz.snowball_melt = self._snowball_melt + spaz.snowball_bounce = self._snowball_bounce + spaz.snowball_explode = self._snowball_explode + + return spaz + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + self._dingsound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + self._dingsound.play() + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/plugins/minigames/soccer.py b/plugins/minigames/soccer.py new file mode 100644 index 000000000..cd9b4f5bc --- /dev/null +++ b/plugins/minigames/soccer.py @@ -0,0 +1,375 @@ +# Released under the MIT License. See LICENSE for details. +# BY Stary_Agent +"""Hockey game and support classes.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + + +class Puck(bs.Actor): + """A lovely giant hockey puck.""" + + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + assert activity is not None + assert isinstance(activity, HockeyGame) + pmats = [shared.object_material, activity.puck_material] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': activity.puck_model, + 'color_texture': activity.puck_tex, + 'body': 'sphere', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.5, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class HockeyGame(bs.TeamGameActivity[Player, Team]): + """Ice hockey game.""" + + name = 'Epic Soccer' + description = 'Score some goals.' + available_settings = [ + bs.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.1), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + assert babase.app.classic is not None + return babase.app.classic.getmaps('football') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self.slow_motion = True + self._scoreboard = Scoreboard() + self._cheer_sound = bui.getsound('cheer') + self._chant_sound = bui.getsound('crowdChant') + self._foghorn_sound = bui.getsound('foghorn') + self._swipsound = bui.getsound('swip') + self._whistle_sound = bui.getsound('refWhistle') + self.puck_model = bs.getmesh('bomb') + self.puck_tex = bs.gettexture('landMine') + self.puck_scored_tex = bs.gettexture('landMineLit') + self._puck_sound = bs.getsound('metalHit') + self.puck_material = bs.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.puck_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._puck_sound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', bs.DieMessage()))) + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[bs.NodeActor]] = None + self._puck: Optional[Puck] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Score a goal.' + return 'Score ${ARG1} goals.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'score a goal' + return 'score ${ARG1} goals', self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._puck_spawn_pos = self.map.get_flag_position(None) + self._spawn_puck() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': defs.boxes['goal1'][0:3], + 'scale': defs.boxes['goal1'][6:9], + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': defs.boxes['goal2'][0:3], + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._update_scoreboard() + self._chant_sound.play() + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = bs.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + puck.last_players_to_touch[player.team.id] = player + + def _kill_puck(self) -> None: + self._puck = None + + def _handle_score(self) -> None: + """A point has been scored.""" + + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = bs.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 20, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + self._foghorn_sound.play() + self._cheer_sound.play() + + self._puck.scored = True + + # Change puck texture to something cool + self._puck.node.color_texture = self.puck_scored_tex + # Kill the puck (it'll respawn itself shortly). + bs.timer(1.0, self._kill_puck) + + light = bs.newnode('light', + attrs={ + 'position': bs.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + bs.timer(1.0, light.delete) + + bs.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + bs.timer(3.0, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + light = bs.newnode('light', + attrs={ + 'position': self._puck_spawn_pos, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) + + def _spawn_puck(self) -> None: + self._swipsound.play() + self._whistle_sound.play() + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) diff --git a/plugins/minigames/squid_race.py b/plugins/minigames/squid_race.py new file mode 100644 index 000000000..d665e5b40 --- /dev/null +++ b/plugins/minigames/squid_race.py @@ -0,0 +1,953 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# +"""Defines Race mini-game.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +from dataclasses import dataclass + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.bomb import Bomb, Blast, ExplodeHitMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict, + Union) + from bascenev1lib.actor.onscreentimer import OnScreenTimer + + +class NewBlast(Blast): + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ExplodeHitMessage): + pass + else: + return super().handlemessage(msg) + + +@dataclass +class RaceMine: + """Holds info about a mine on the track.""" + point: Sequence[float] + mine: Optional[Bomb] + + +class RaceRegion(bs.Actor): + """Region used to track progress during a race.""" + + def __init__(self, pt: Sequence[float], index: int): + super().__init__() + activity = self.activity + assert isinstance(activity, RaceGame) + self.pos = pt + self.index = index + self.node = bs.newnode( + 'region', + delegate=self, + attrs={ + 'position': pt[:3], + 'scale': (pt[3] * 2.0, pt[4] * 2.0, pt[5] * 2.0), + 'type': 'box', + 'materials': [activity.race_region_material] + }) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.distance_txt: Optional[bs.Node] = None + self.last_region = 0 + self.lap = 0 + self.distance = 0.0 + self.finished = False + self.rank: Optional[int] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.time: Optional[float] = None + self.lap = 0 + self.finished = False + + +# ba_meta export bascenev1.GameActivity +class SquidRaceGame(bs.TeamGameActivity[Player, Team]): + """Game of racing around a track.""" + + name = 'Squid Race' + description = 'Run real fast!' + scoreconfig = bs.ScoreConfig(label='Time', + lower_is_better=True, + scoretype=bs.ScoreType.MILLISECONDS) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting('Laps', min_value=1, default=3, increment=1), + bs.IntChoiceSetting( + 'Time Limit', + default=0, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + ), + bs.IntChoiceSetting( + 'Mine Spawning', + default=4000, + choices=[ + ('No Mines', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ], + ), + bs.IntChoiceSetting( + 'Bomb Spawning', + choices=[ + ('None', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ('1 Second', 1000), + ], + default=2000, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + + # We have some specific settings in teams mode. + if issubclass(sessiontype, bs.DualTeamSession): + settings.append( + bs.BoolSetting('Entire Team Must Finish', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.MultiTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('race') + + def __init__(self, settings: dict): + self._race_started = False + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_sound = bs.getsound('score') + self._swipsound = bs.getsound('swip') + self._last_team_time: Optional[float] = None + self._front_race_region: Optional[int] = None + self._nub_tex = bs.gettexture('nub') + self._beep_1_sound = bs.getsound('raceBeep1') + self._beep_2_sound = bs.getsound('raceBeep2') + self.race_region_material: Optional[bs.Material] = None + self._regions: List[RaceRegion] = [] + self._team_finish_pts: Optional[int] = None + self._time_text: Optional[bs.Actor] = None + self._timer: Optional[OnScreenTimer] = None + self._race_mines: Optional[List[RaceMine]] = None + self._race_mine_timer: Optional[bs.Timer] = None + self._scoreboard_timer: Optional[bs.Timer] = None + self._player_order_update_timer: Optional[bs.Timer] = None + self._start_lights: Optional[List[bs.Node]] = None + self._squid_lights: Optional[List[bs.Node]] = None + self._countdown_timer: int = 0 + self._sq_mode: str = 'Easy' + self._tick_timer: Optional[bs.Timer] = None + self._bomb_spawn_timer: Optional[bs.Timer] = None + self._laps = int(settings['Laps']) + self._entire_team_must_finish = bool( + settings.get('Entire Team Must Finish', False)) + self._time_limit = float(settings['Time Limit']) + self._mine_spawning = int(settings['Mine Spawning']) + self._bomb_spawning = int(settings['Bomb Spawning']) + self._epic_mode = bool(settings['Epic Mode']) + + self._countdownsounds = { + 10: bs.getsound('announceTen'), + 9: bs.getsound('announceNine'), + 8: bs.getsound('announceEight'), + 7: bs.getsound('announceSeven'), + 6: bs.getsound('announceSix'), + 5: bs.getsound('announceFive'), + 4: bs.getsound('announceFour'), + 3: bs.getsound('announceThree'), + 2: bs.getsound('announceTwo'), + 1: bs.getsound('announceOne') + } + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC_RACE + if self._epic_mode else bs.MusicType.RACE) + + def get_instance_description(self) -> Union[str, Sequence]: + if (isinstance(self.session, bs.DualTeamSession) + and self._entire_team_must_finish): + t_str = ' Your entire team has to finish.' + else: + t_str = '' + + if self._laps > 1: + return 'Run ${ARG1} laps.' + t_str, self._laps + return 'Run 1 lap.' + t_str + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._laps > 1: + return 'run ${ARG1} laps', self._laps + return 'run 1 lap' + + def on_transition_in(self) -> None: + super().on_transition_in() + shared = SharedObjects.get() + pts = self.map.get_def_points('race_point') + mat = self.race_region_material = bs.Material() + mat.add_actions(conditions=('they_have_material', + shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + self._handle_race_point_collide), + )) + for rpt in pts: + self._regions.append(RaceRegion(rpt, len(self._regions))) + + def _flash_player(self, player: Player, scale: float) -> None: + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + pos = player.actor.node.position + light = bs.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + bs.timer(0.5, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) + + def _handle_race_point_collide(self) -> None: + # FIXME: Tidy this up. + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + collision = bs.getcollision() + try: + region = collision.sourcenode.getdelegate(RaceRegion, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + last_region = player.last_region + this_region = region.index + + if last_region != this_region: + + # If a player tries to skip regions, smite them. + # Allow a one region leeway though (its plausible players can get + # blown over a region, etc). + if this_region > last_region + 2: + if player.is_alive(): + assert player.actor + player.actor.handlemessage(bs.DieMessage()) + bs.broadcastmessage(babase.Lstr( + translate=('statements', 'Killing ${NAME} for' + ' skipping part of the track!'), + subs=[('${NAME}', player.getname(full=True))]), + color=(1, 0, 0)) + else: + # If this player is in first, note that this is the + # front-most race-point. + if player.rank == 0: + self._front_race_region = this_region + + player.last_region = this_region + if last_region >= len(self._regions) - 2 and this_region == 0: + team = player.team + player.lap = min(self._laps, player.lap + 1) + + # In teams mode with all-must-finish on, the team lap + # value is the min of all team players. + # Otherwise its the max. + if isinstance(self.session, bs.DualTeamSession + ) and self._entire_team_must_finish: + team.lap = min([p.lap for p in team.players]) + else: + team.lap = max([p.lap for p in team.players]) + + # A player is finishing. + if player.lap == self._laps: + + # In teams mode, hand out points based on the order + # players come in. + if isinstance(self.session, bs.DualTeamSession): + assert self._team_finish_pts is not None + if self._team_finish_pts > 0: + self.stats.player_scored(player, + self._team_finish_pts, + screenmessage=False) + self._team_finish_pts -= 25 + + # Flash where the player is. + self._flash_player(player, 1.0) + player.finished = True + assert player.actor + player.actor.handlemessage( + bs.DieMessage(immediate=True)) + + # Makes sure noone behind them passes them in rank + # while finishing. + player.distance = 9999.0 + + # If the whole team has finished the race. + if team.lap == self._laps: + self._score_sound.play() + player.team.finished = True + assert self._timer is not None + elapsed = bs.time() - self._timer.getstarttime() + self._last_team_time = player.team.time = elapsed + + # Team has yet to finish. + else: + self._swipsound.play() + + # They've just finished a lap but not the race. + else: + self._swipsound.play() + self._flash_player(player, 0.3) + + # Print their lap number over their head. + try: + assert isinstance(player.actor, PlayerSpaz) + mathnode = bs.newnode('math', + owner=player.actor.node, + attrs={ + 'input1': (0, 1.9, 0), + 'operation': 'add' + }) + player.actor.node.connectattr( + 'torso_position', mathnode, 'input2') + tstr = babase.Lstr(resource='lapNumberText', + subs=[('${CURRENT}', + str(player.lap + 1)), + ('${TOTAL}', str(self._laps)) + ]) + txtnode = bs.newnode('text', + owner=mathnode, + attrs={ + 'text': tstr, + 'in_world': True, + 'color': (1, 1, 0, 1), + 'scale': 0.015, + 'h_align': 'center' + }) + mathnode.connectattr('output', txtnode, 'position') + bs.animate(txtnode, 'scale', { + 0.0: 0, + 0.2: 0.019, + 2.0: 0.019, + 2.2: 0 + }) + bs.timer(2.3, mathnode.delete) + except Exception: + babase.print_exception('Error printing lap.') + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_join(self, player: Player) -> None: + # Don't allow joining after we start + # (would enable leave/rejoin tomfoolery). + if self.has_begun(): + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + return + self.spawn_player(player) + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + + # A player leaving disqualifies the team if 'Entire Team Must Finish' + # is on (otherwise in teams mode everyone could just leave except the + # leading player to win). + if (isinstance(self.session, bs.DualTeamSession) + and self._entire_team_must_finish): + bs.broadcastmessage(babase.Lstr( + translate=('statements', + '${TEAM} is disqualified because ${PLAYER} left'), + subs=[('${TEAM}', player.team.name), + ('${PLAYER}', player.getname(full=True))]), + color=(1, 1, 0)) + player.team.finished = True + player.team.time = None + player.team.lap = 0 + bs.getsound('boo').play() + for otherplayer in player.team.players: + otherplayer.lap = 0 + otherplayer.finished = True + try: + if otherplayer.actor is not None: + otherplayer.actor.handlemessage(bs.DieMessage()) + except Exception: + babase.print_exception('Error sending DieMessage.') + + # Defer so team/player lists will be updated. + babase.pushcall(self._check_end_game) + + def _update_scoreboard(self) -> None: + for team in self.teams: + distances = [player.distance for player in team.players] + if not distances: + teams_dist = 0.0 + else: + if (isinstance(self.session, bs.DualTeamSession) + and self._entire_team_must_finish): + teams_dist = min(distances) + else: + teams_dist = max(distances) + self._scoreboard.set_team_value( + team, + teams_dist, + self._laps, + flash=(teams_dist >= float(self._laps)), + show_value=False) + + def on_begin(self) -> None: + from bascenev1lib.actor.onscreentimer import OnScreenTimer + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._team_finish_pts = 100 + + # Throw a timer up on-screen. + self._time_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.5, 1), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, -50), + 'scale': 1.4, + 'text': '' + })) + self._timer = OnScreenTimer() + + if self._mine_spawning != 0: + self._race_mines = [ + RaceMine(point=p, mine=None) + for p in self.map.get_def_points('race_mine') + ] + if self._race_mines: + self._race_mine_timer = bs.Timer(0.001 * self._mine_spawning, + self._update_race_mine, + repeat=True) + + self._scoreboard_timer = bs.Timer(0.25, + self._update_scoreboard, + repeat=True) + self._player_order_update_timer = bs.Timer(0.25, + self._update_player_order, + repeat=True) + + if self.slow_motion: + t_scale = 0.4 + light_y = 50 + else: + t_scale = 1.0 + light_y = 150 + lstart = 7.1 * t_scale + inc = 1.25 * t_scale + + bs.timer(lstart, self._do_light_1) + bs.timer(lstart + inc, self._do_light_2) + bs.timer(lstart + 2 * inc, self._do_light_3) + bs.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = bs.newnode('image', + attrs={ + 'texture': bs.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + bs.animate( + lnub, 'opacity', { + 4.0 * t_scale: 0, + 5.0 * t_scale: 1.0, + 12.0 * t_scale: 1.0, + 12.5 * t_scale: 0.0 + }) + bs.timer(13.0 * t_scale, lnub.delete) + self._start_lights.append(lnub) + + self._start_lights[0].color = (0.2, 0, 0) + self._start_lights[1].color = (0.2, 0, 0) + self._start_lights[2].color = (0.2, 0.05, 0) + self._start_lights[3].color = (0.0, 0.3, 0) + + self._squid_lights = [] + for i in range(2): + lnub = bs.newnode('image', + attrs={ + 'texture': bs.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-33 + i * 65, 220), + 'scale': (60, 60), + 'attach': 'center' + }) + bs.animate( + lnub, 'opacity', { + 4.0 * t_scale: 0, + 5.0 * t_scale: 1.0}) + self._squid_lights.append(lnub) + self._squid_lights[0].color = (0.2, 0, 0) + self._squid_lights[1].color = (0.0, 0.3, 0) + + bs.timer(1.0, self._check_squid_end, repeat=True) + self._squidgame_countdown() + + def _squidgame_countdown(self) -> None: + self._countdown_timer = 80 * self._laps # 80 + bs.newnode( + 'image', + attrs={ + 'opacity': 0.7, + 'color': (0.2, 0.2, 0.2), + 'attach': 'topCenter', + 'position': (-220, -40), + 'scale': (135, 45), + 'texture': bs.gettexture('bar')}) + bs.newnode( + 'image', + attrs={ + 'opacity': 1.0, + 'color': (1.0, 0.0, 0.0), + 'attach': 'topCenter', + 'position': (-220, -38), + 'scale': (155, 65), + 'texture': bs.gettexture('uiAtlas'), + 'mesh_transparent': bs.getmesh('meterTransparent')}) + self._sgcountdown_text = bs.newnode( + 'text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'position': (-220, -57), + 'shadow': 0.5, + 'flatness': 0.5, + 'scale': 1.1, + 'text': str(self._countdown_timer)+"s"}) + + def _update_sgcountdown(self) -> None: + self._countdown_timer -= 1 + self._countdown_timer + if self._countdown_timer <= 0: + self._countdown_timer = 0 + self._squid_game_all_die() + if self._countdown_timer == 20: + self._sq_mode = 'Hard' + bs.getsound('alarm').play() + if self._countdown_timer == 40: + self._sq_mode = 'Normal' + if self._countdown_timer <= 20: + self._sgcountdown_text.color = (1.2, 0.0, 0.0) + self._sgcountdown_text.scale = 1.2 + if self._countdown_timer in self._countdownsounds: + self._countdownsounds[self._countdown_timer].play() + else: + self._sgcountdown_text.color = (1.0, 1.0, 1.0) + self._sgcountdown_text.text = str(self._countdown_timer)+"s" + + def _squid_game_all_die(self) -> None: + for player in self.players: + if player.is_alive(): + player.actor._cursed = True + player.actor.handlemessage(bs.DieMessage()) + NewBlast( + position=player.actor.node.position, + velocity=player.actor.node.velocity, + blast_radius=3.0, + blast_type='normal').autoretain() + player.actor.handlemessage( + bs.HitMessage( + pos=player.actor.node.position, + velocity=player.actor.node.velocity, + magnitude=2000, + hit_type='explosion', + hit_subtype='normal', + radius=2.0, + source_player=None)) + player.actor._cursed = False + + def _do_ticks(self) -> None: + def do_ticks(): + if self._ticks: + bs.getsound('tick').play() + self._tick_timer = bs.timer(1.0, do_ticks, repeat=True) + + def _start_squid_game(self) -> None: + easy = [4.5, 5, 5.5, 6] + normal = [4, 4.5, 5] + hard = [3, 3.5, 4] + random_number = random.choice( + hard if self._sq_mode == 'Hard' else + normal if self._sq_mode == 'Normal' else easy) + # if random_number == 6: + # bs.getsound('lrlg_06s').play() + # elif random_number == 5.5: + # bs.getsound('lrlg_055s').play() + # elif random_number == 5: + # bs.getsound('lrlg_05s').play() + # elif random_number == 4.5: + # bs.getsound('lrlg_045s').play() + # elif random_number == 4: + # bs.getsound('lrlg_04s').play() + # elif random_number == 3.5: + # bs.getsound('lrlg_035s').play() + # elif random_number == 3: + # bs.getsound('lrlg_03s').play() + self._squid_lights[0].color = (0.2, 0, 0) + self._squid_lights[1].color = (0.0, 1.0, 0) + self._do_delete = False + self._ticks = True + bs.timer(random_number, self._stop_squid_game) + + def _stop_squid_game(self) -> None: + self._ticks = False + self._squid_lights[0].color = (1.0, 0, 0) + self._squid_lights[1].color = (0.0, 0.3, 0) + bs.timer(0.2, self._check_delete) + + def _check_delete(self) -> None: + for player in self.players: + if player.is_alive(): + player.customdata['position'] = None + player.customdata['position'] = player.actor.node.position + self._do_delete = True + bs.timer(3.0 if self._sq_mode == 'Hard' else 4.0, + self._start_squid_game) + + def _start_delete(self) -> None: + for player in self.players: + if player.is_alive() and self._do_delete: + + posx = float("%.1f" % player.customdata['position'][0]) + posz = float("%.1f" % player.customdata['position'][1]) + posy = float("%.1f" % player.customdata['position'][2]) + + posx_list = [ + round(posx, 1), round(posx+0.1, 1), round(posx+0.2, 1), + round(posx-0.1, 1), round(posx-0.2, 1)] + current_posx = float("%.1f" % player.actor.node.position[0]) + + posz_list = [ + round(posz, 1), round(posz+0.1, 1), round(posz+0.2, 1), + round(posz-0.1, 1), round(posz-0.2, 1)] + current_posz = float("%.1f" % player.actor.node.position[1]) + + posy_list = [ + round(posy, 1), round(posy+0.1, 1), round(posy+0.2, 1), + round(posy-0.1, 1), round(posy-0.2, 1)] + current_posy = float("%.1f" % player.actor.node.position[2]) + + if not (current_posx in posx_list) or not ( + current_posz in posz_list) or not ( + current_posy in posy_list): + player.actor._cursed = True + player.actor.handlemessage(bs.DieMessage()) + NewBlast( + position=player.actor.node.position, + velocity=player.actor.node.velocity, + blast_radius=3.0, + blast_type='normal').autoretain() + player.actor.handlemessage( + bs.HitMessage( + pos=player.actor.node.position, + velocity=player.actor.node.velocity, + magnitude=2000, + hit_type='explosion', + hit_subtype='normal', + radius=2.0, + source_player=None)) + player.actor._cursed = False + + def _check_squid_end(self) -> None: + squid_player_alive = 0 + for player in self.players: + if player.is_alive(): + squid_player_alive += 1 + break + if squid_player_alive < 1: + self.end_game() + + def _do_light_1(self) -> None: + assert self._start_lights is not None + self._start_lights[0].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + self._beep_1_sound.play() + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + self._beep_2_sound.play() + for player in self.players: + if player.actor is not None: + try: + assert isinstance(player.actor, PlayerSpaz) + player.actor.connect_controls_to_player() + except Exception: + babase.print_exception('Error in race player connects.') + assert self._timer is not None + self._timer.start() + + if self._bomb_spawning != 0: + self._bomb_spawn_timer = bs.Timer(0.001 * self._bomb_spawning, + self._spawn_bomb, + repeat=True) + + self._race_started = True + self._squid_lights[1].color = (0.0, 1.0, 0) + self._start_squid_game() + self._do_ticks() + bs.timer(0.2, self._start_delete, repeat=True) + bs.timer(1.0, self._update_sgcountdown, repeat=True) + + def _update_player_order(self) -> None: + + # Calc all player distances. + for player in self.players: + pos: Optional[babase.Vec3] + try: + pos = player.position + except bs.NotFoundError: + pos = None + if pos is not None: + r_index = player.last_region + rg1 = self._regions[r_index] + r1pt = babase.Vec3(rg1.pos[:3]) + rg2 = self._regions[0] if r_index == len( + self._regions) - 1 else self._regions[r_index + 1] + r2pt = babase.Vec3(rg2.pos[:3]) + r2dist = (pos - r2pt).length() + amt = 1.0 - (r2dist / (r2pt - r1pt).length()) + amt = player.lap + (r_index + amt) * (1.0 / len(self._regions)) + player.distance = amt + + # Sort players by distance and update their ranks. + p_list = [(player.distance, player) for player in self.players] + + p_list.sort(reverse=True, key=lambda x: x[0]) + for i, plr in enumerate(p_list): + plr[1].rank = i + if plr[1].actor: + node = plr[1].distance_txt + if node: + node.text = str(i + 1) if plr[1].is_alive() else '' + + def _spawn_bomb(self) -> None: + if self._front_race_region is None: + return + region = (self._front_race_region + 3) % len(self._regions) + pos = self._regions[region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1] + 1.0, + pos[2] + random.uniform(*z_range)) + bs.timer(random.uniform(0.0, 2.0), + bs.WeakCall(self._spawn_bomb_at_pos, pos)) + + def _spawn_bomb_at_pos(self, pos: Sequence[float]) -> None: + if self.has_ended(): + return + Bomb(position=pos, bomb_type='normal').autoretain() + + def _make_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + rmine.mine = Bomb(position=rmine.point[:3], bomb_type='land_mine') + rmine.mine.arm() + + def _flash_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + light = bs.newnode('light', + attrs={ + 'position': rmine.point[:3], + 'color': (1, 0.2, 0.2), + 'radius': 0.1, + 'height_attenuated': False + }) + bs.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) + bs.timer(1.0, light.delete) + + def _update_race_mine(self) -> None: + assert self._race_mines is not None + m_index = -1 + rmine = None + for _i in range(3): + m_index = random.randrange(len(self._race_mines)) + rmine = self._race_mines[m_index] + if not rmine.mine: + break + assert rmine is not None + if not rmine.mine: + self._flash_mine(m_index) + bs.timer(0.95, babase.Call(self._make_mine, m_index)) + + def spawn_player(self, player: Player) -> bs.Actor: + if player.team.finished: + # FIXME: This is not type-safe! + # This call is expected to always return an Actor! + # Perhaps we need something like can_spawn_player()... + # noinspection PyTypeChecker + return None # type: ignore + pos = self._regions[player.last_region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1], + pos[2] + random.uniform(*z_range)) + spaz = self.spawn_player_spaz( + player, position=pos, angle=90 if not self._race_started else None) + assert spaz.node + + # Prevent controlling of characters before the start of the race. + if not self._race_started: + spaz.disconnect_controls_from_player() + + mathnode = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + spaz.node.connectattr('torso_position', mathnode, 'input2') + + distance_txt = bs.newnode('text', + owner=spaz.node, + attrs={ + 'text': '', + 'in_world': True, + 'color': (1, 1, 0.4), + 'scale': 0.02, + 'h_align': 'center' + }) + player.distance_txt = distance_txt + mathnode.connectattr('output', distance_txt, 'position') + return spaz + + def _check_end_game(self) -> None: + + # If there's no teams left racing, finish. + teams_still_in = len([t for t in self.teams if not t.finished]) + if teams_still_in == 0: + self.end_game() + return + + # Count the number of teams that have completed the race. + teams_completed = len( + [t for t in self.teams if t.finished and t.time is not None]) + + if teams_completed > 0: + session = self.session + + # In teams mode its over as soon as any team finishes the race + + # FIXME: The get_ffa_point_awards code looks dangerous. + if isinstance(session, bs.DualTeamSession): + self.end_game() + else: + # In ffa we keep the race going while there's still any points + # to be handed out. Find out how many points we have to award + # and how many teams have finished, and once that matches + # we're done. + assert isinstance(session, bs.FreeForAllSession) + points_to_award = len(session.get_ffa_point_awards()) + if teams_completed >= points_to_award - teams_completed: + self.end_game() + return + + def end_game(self) -> None: + + # Stop updating our time text, and set it to show the exact last + # finish time if we have one. (so users don't get upset if their + # final time differs from what they see onscreen by a tiny amount) + assert self._timer is not None + if self._timer.has_started(): + self._timer.stop( + endtime=None if self._last_team_time is None else ( + self._timer.getstarttime() + self._last_team_time)) + + results = bs.GameResults() + + for team in self.teams: + if team.time is not None: + # We store time in seconds, but pass a score in milliseconds. + results.set_team_score(team, int(team.time * 1000.0)) + else: + results.set_team_score(team, None) + + # We don't announce a winner in ffa mode since its probably been a + # while since the first place guy crossed the finish line so it seems + # odd to be announcing that now. + self.end(results=results, + announce_winning_team=isinstance(self.session, + bs.DualTeamSession)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # Augment default behavior. + super().handlemessage(msg) + else: + super().handlemessage(msg) diff --git a/plugins/minigames/super_duel.py b/plugins/minigames/super_duel.py new file mode 100644 index 000000000..b6fc23947 --- /dev/null +++ b/plugins/minigames/super_duel.py @@ -0,0 +1,602 @@ +"""New Duel / Created by: byANG3L""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.game.elimination import Icon + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + + +class SuperSpaz(PlayerSpaz): + + def __init__(self, + player: bs.Player, + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + character: str = 'Spaz', + super_punch: bool = False, + powerups_expire: bool = True): + super().__init__(player=player, + color=color, + highlight=highlight, + character=character, + powerups_expire=powerups_expire) + self._super_punch = super_punch + + def handlemessage(self, msg: Any) -> Any: + from bascenev1lib.actor.spaz import PunchHitMessage + from bascenev1lib.actor.bomb import Blast + if isinstance(msg, PunchHitMessage): + super().handlemessage(msg) + node = bs.getcollision().opposingnode + if self._super_punch: + if node.getnodetype() == 'spaz': + if not node.frozen: + node.frozen = True + node.handlemessage(babase.FreezeMessage()) + bs.getsound('freeze').play() + bs.getsound('superPunch').play() + bs.getsound('punchStrong02').play() + Blast(position=node.position, + velocity=node.velocity, + blast_radius=0.0, + blast_type='normal').autoretain() + else: + return super().handlemessage(msg) + return None + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.icons: List[Icon] = [] + self.in_game: bool = False + self.playervs1: bool = False + self.playervs2: bool = False + self.light: bool = False + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +lang = bs.app.lang.language +if lang == 'Spanish': + enable_powerups = 'Habilitar Potenciadores' + night_mode = 'Modo Noche' + fight_delay = 'Tiempo entre Pelea' + very_fast = 'Muy Rápido' + fast = 'Rápido' + normal = 'Normal' + slow = 'Lento' + very_slow = 'Muy Lento' + none = 'Ninguno' + super_punch = 'Super Golpe' + box_mode = 'Modo Caja' + boxing_gloves = 'Guantes de Boxeo' +else: + enable_powerups = 'Enable Powerups' + night_mode = 'Night Mode' + fight_delay = 'Fight Delay' + very_fast = 'Very Fast' + fast = 'Fast' + normal = 'Normal' + slow = 'Slow' + very_slow = 'Very Slow' + super_punch = 'Super Punch' + box_mode = 'Box Mode' + boxing_gloves = 'Boxing Gloves' + +# ba_meta export bascenev1.GameActivity + + +class NewDuelGame(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Duel' + description = 'Kill a set number of enemies to win.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.BoolSetting(enable_powerups, default=False), + bs.BoolSetting(boxing_gloves, default=False), + bs.BoolSetting(night_mode, default=False), + bs.BoolSetting(super_punch, default=False), + bs.BoolSetting(box_mode, default=False), + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Allow Negative Scores', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._enable_powerups = bool(settings[enable_powerups]) + self._night_mode = bool(settings[night_mode]) + self._fight_delay: float = 0 + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + self._super_punch = bool(settings[super_punch]) + self._box_mode = bool(settings[box_mode]) + self._boxing_gloves = bool(settings[boxing_gloves]) + self._vs_text: Optional[bs.Actor] = None + self.spawn_order: List[Player] = [] + self._players_vs_1: bool = False + self._players_vs_2: bool = False + self._first_countdown: bool = True + self._count_1 = bs.getsound('announceOne') + self._count_2 = bs.getsound('announceTwo') + self._count_3 = bs.getsound('announceThree') + self._boxing_bell = bs.getsound('boxingBell') + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Crush ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies', self._score_to_win + + def on_player_join(self, player: Player) -> None: + self.spawn_order.append(player) + self._update_order() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + player.icons = [] + if player.playervs1: + player.playervs1 = False + self._players_vs_1 = False + player.in_game = False + elif player.playervs2: + player.playervs2 = False + self._players_vs_2 = False + player.in_game = False + if player in self.spawn_order: + self.spawn_order.remove(player) + bs.timer(0.2, self._update_order) + + def on_transition_in(self) -> None: + super().on_transition_in() + if self._night_mode: + gnode = bs.getactivity().globalsnode + gnode.tint = (0.3, 0.3, 0.3) + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + if self._enable_powerups: + self.setup_standard_powerup_drops() + self._vs_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': babase.Lstr(resource='vsText') + })) + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + bs.timer(1.0, self._update, repeat=True) + + def _update(self) -> None: + if len(self.players) == 1: + 'self.end_game()' + + def spawn_player(self, player: PlayerType) -> bs.Actor: + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from babase import _math + from bascenev1._coopsession import CoopSession + from bascenev1lib.actor.spazfactory import SpazFactory + factory = SpazFactory.get() + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = babase.safecolor(color, target_intensity=0.75) + spaz = SuperSpaz(color=color, + highlight=highlight, + character=player.character, + player=player, + super_punch=True if self._super_punch else False) + + player.actor = spaz + assert spaz.node + + # If this is co-op and we're on Courtyard or Runaround, add the + # material that allows us to collide with the player-walls. + # FIXME: Need to generalize this. + if isinstance(self.session, CoopSession) and self.map.getname() in [ + 'Courtyard', 'Tower D' + ]: + mat = self.map.preloaddata['collide_with_wall_material'] + assert isinstance(spaz.node.materials, tuple) + assert isinstance(spaz.node.roller_materials, tuple) + spaz.node.materials += (mat, ) + spaz.node.roller_materials += (mat, ) + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + pos1 = [self.map.get_start_position(0), 90] + pos2 = [self.map.get_start_position(1), 270] + pos3 = [] + + for x in self.players: + if x.is_alive(): + if x is player: + continue + p = x.actor.node.position + if 0.0 not in (p[0], p[2]): + if p[0] <= 0: + pos3.append(pos2[0]) + else: + pos3.append(pos1[0]) + + spaz.handlemessage(bs.StandMessage(pos1[0] if player.playervs1 else pos2[0], + pos1[1] if player.playervs1 else pos2[1])) + + if any(pos3): + spaz.handlemessage(bs.StandMessage(pos3[0])) + + if self._super_punch: + spaz._punch_power_scale = factory.punch_power_scale_gloves = 10 + spaz.equip_boxing_gloves() + lfx = bs.newnode( + 'light', + attrs={ + 'color': color, + 'radius': 0.3, + 'intensity': 0.3}) + + def sp_fx(): + if not spaz.node: + lfx.delete() + return + bs.emitfx(position=spaz.node.position, + velocity=spaz.node.velocity, + count=5, + scale=0.5, + spread=0.5, + chunk_type='spark') + bs.emitfx(position=spaz.node.position, + velocity=spaz.node.velocity, + count=2, + scale=0.8, + spread=0.3, + chunk_type='spark') + if lfx: + spaz.node.connectattr('position', lfx, 'position') + bs.timer(0.1, sp_fx, repeat=True) + + if self._box_mode: + spaz.node.color_texture = bs.gettexture('tnt') + spaz.node.color_mask_texture = bs.gettexture('tnt') + spaz.node.color = (1, 1, 1) + spaz.node.highlight = (1, 1, 1) + spaz.node.head_mesh = None + spaz.node.torso_mesh = bs.getmesh('tnt') + spaz.node.style = 'cyborg' + + if self._boxing_gloves: + spaz.equip_boxing_gloves() + + return spaz + + def _update_spawn(self) -> None: + if self._players_vs_1 or self._players_vs_2: + for player in self.players: + if player.playervs1 or player.playervs2: + if not player.is_alive(): + self.spawn_player(player) + # player.actor.disconnect_controls_from_player() + + if self._night_mode: + if not player.light: + player.light = True + light = bs.newnode( + 'light', + owner=player.node, + attrs={ + 'radius': 0.3, + 'intensity': 0.6, + 'height_attenuated': False, + 'color': player.color + }) + player.node.connectattr( + 'position', light, 'position') + else: + player.actor.disconnect_controls_from_player() + + bs.timer(0.0, self._countdown) + # bs.timer(0.1, self._clear_all_objects) + + def _countdown(self) -> None: + self._first_countdown = False + if self._fight_delay == 0: + for player in self.players: + if player.playervs1 or player.playervs2: + if not player.is_alive(): + return + else: + player.actor.connect_controls_to_player() + else: + bs.timer(self._fight_delay, self.count3) + + def start(self) -> None: + self._count_text('FIGHT') + self._boxing_bell.play() + for player in self.players: + if player.playervs1 or player.playervs2: + if not player.is_alive(): + return + else: + player.actor.connect_controls_to_player() + + def count(self) -> None: + self._count_text('1') + self._count_1.play() + bs.timer(self._fight_delay, self.start) + + def count2(self) -> None: + self._count_text('2') + self._count_2.play() + bs.timer(self._fight_delay, self.count) + + def count3(self) -> None: + self._count_text('3') + self._count_3.play() + bs.timer(self._fight_delay, self.count2) + + def _count_text(self, num: str) -> None: + self.node = bs.newnode('text', + attrs={ + 'v_attach': 'center', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.5, 1), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, 18), + 'text': num + }) + if self._fight_delay == 0.7: + bs.animate(self.node, 'scale', + {0: 0, 0.1: 3.9, 0.64: 4.3, 0.68: 0}) + elif self._fight_delay == 0.4: + bs.animate(self.node, 'scale', + {0: 0, 0.1: 3.9, 0.34: 4.3, 0.38: 0}) + else: + bs.animate(self.node, 'scale', + {0: 0, 0.1: 3.9, 0.92: 4.3, 0.96: 0}) + cmb = bs.newnode('combine', owner=self.node, attrs={'size': 4}) + cmb.connectattr('output', self.node, 'color') + bs.animate(cmb, 'input0', {0: 1.0, 0.15: 1.0}, loop=True) + bs.animate(cmb, 'input1', {0: 1.0, 0.15: 0.5}, loop=True) + bs.animate(cmb, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) + cmb.input3 = 1.0 + bs.timer(self._fight_delay, self.node.delete) + + def _update_order(self) -> None: + for player in self.spawn_order: + assert isinstance(player, Player) + if not player.is_alive(): + if not self._players_vs_1: + self._players_vs_1 = True + player.playervs1 = True + player.in_game = True + self.spawn_order.remove(player) + self._update_spawn() + elif not self._players_vs_2: + self._players_vs_2 = True + player.playervs2 = True + player.in_game = True + self.spawn_order.remove(player) + self._update_spawn() + self._update_icons() + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + + for player in self.players: + player.icons = [] + + if player.in_game: + if player.playervs1: + xval = -60 + x_offs = -78 + elif player.playervs2: + xval = 60 + x_offs = 78 + player.icons.append( + Icon(player, + position=(xval, 40), + scale=1.0, + name_maxwidth=130, + name_scale=0.8, + flatness=0.0, + shadow=0.5, + show_death=True, + show_lives=False)) + else: + xval = 125 + xval2 = -125 + x_offs = 78 + for player in self.spawn_order: + player.icons.append( + Icon(player, + position=(xval, 25), + scale=0.5, + name_maxwidth=75, + name_scale=1.0, + flatness=1.0, + shadow=1.0, + show_death=False, + show_lives=False)) + xval += x_offs * 0.56 + player.icons.append( + Icon(player, + position=(xval2, 25), + scale=0.5, + name_maxwidth=75, + name_scale=1.0, + flatness=1.0, + shadow=1.0, + show_death=False, + show_lives=False)) + xval2 -= x_offs * 0.56 + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + + if player.playervs1: + player.playervs1 = False + self._players_vs_1 = False + player.in_game = False + self.spawn_order.append(player) + elif player.playervs2: + player.playervs2 = False + self._players_vs_2 = False + player.in_game = False + self.spawn_order.append(player) + bs.timer(0.2, self._update_order) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + self._dingsound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + self._dingsound.play() + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/plugins/minigames/supersmash.py b/plugins/minigames/supersmash.py new file mode 100644 index 000000000..35838d651 --- /dev/null +++ b/plugins/minigames/supersmash.py @@ -0,0 +1,956 @@ +# To learn more, see https://ballistica.net/wiki/meta-tag-system +# ba_meta require api 8 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import random +import bauiv1 as bui +import bascenev1 as bs +from babase import _math +from bascenev1lib.actor.spaz import Spaz +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.game import elimination +from bascenev1lib.game.elimination import Icon, Player, Team +from bascenev1lib.actor.bomb import Bomb, Blast +from bascenev1lib.actor.playerspaz import PlayerSpaz, PlayerSpazHurtMessage + +if TYPE_CHECKING: + from typing import Any, Type, List, Sequence, Optional + + +class Icon(Icon): + def update_for_lives(self) -> None: + """Update for the target player's current lives.""" + if self._player: + lives = self._player.lives + else: + lives = 0 + if self._show_lives: + if lives > 1: + self._lives_text.text = 'x' + str(lives - 1) + else: + self._lives_text.text = '' + if lives == 0: + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + +class PowBox(Bomb): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0)) -> None: + Bomb.__init__(self, + position, + velocity, + bomb_type='tnt', + blast_radius=2.5, + source_player=None, + owner=None) + self.set_pow_text() + + def set_pow_text(self) -> None: + m = bs.newnode('math', + owner=self.node, + attrs={'input1': (0, 0.7, 0), + 'operation': 'add'}) + self.node.connectattr('position', m, 'input2') + + self._pow_text = bs.newnode('text', + owner=self.node, + attrs={'text': 'POW!', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (1, 1, 0.4), + 'scale': 0.0, + 'h_align': 'center'}) + m.connectattr('output', self._pow_text, 'position') + bs.animate(self._pow_text, 'scale', {0: 0.0, 1.0: 0.01}) + + def pow(self) -> None: + self.explode() + + def handlemessage(self, m: Any) -> Any: + if isinstance(m, bs.PickedUpMessage): + self._heldBy = m.node + elif isinstance(m, bs.DroppedMessage): + bs.timer(0.6, self.pow) + Bomb.handlemessage(self, m) + + +class SSPlayerSpaz(PlayerSpaz): + multiplyer = 1 + is_dead = False + + def oob_effect(self) -> None: + if self.is_dead: + return + self.is_dead = True + if self.multiplyer > 1.25: + blast_type = 'tnt' + radius = min(self.multiplyer * 5, 20) + else: + # penalty for killing people with low multiplyer + blast_type = 'ice' + radius = 7.5 + Blast(position=self.node.position, + blast_radius=radius, + blast_type=blast_type).autoretain() + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.HitMessage): + if not self.node: + return None + if self.node.invincible: + SpazFactory.get().block_sound.play(1.0, position=self.node.position) + return True + + # If we were recently hit, don't count this as another. + # (so punch flurries and bomb pileups essentially count as 1 hit) + local_time = int(bs.time() * 1000) + assert isinstance(local_time, int) + if (self._last_hit_time is None + or local_time - self._last_hit_time > 1000): + self._num_times_hit += 1 + self._last_hit_time = local_time + + mag = msg.magnitude * self.impact_scale + velocity_mag = msg.velocity_magnitude * self.impact_scale + damage_scale = 0.22 + + # If they've got a shield, deliver it to that instead. + if self.shield: + if msg.flat_damage: + damage = msg.flat_damage * self.impact_scale + else: + # Hit our spaz with an impulse but tell it to only return + # theoretical damage; not apply the impulse. + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], mag, + velocity_mag, msg.radius, 1, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + damage = damage_scale * self.node.damage + + assert self.shield_hitpoints is not None + self.shield_hitpoints -= int(damage) + self.shield.hurt = ( + 1.0 - + float(self.shield_hitpoints) / self.shield_hitpoints_max) + + # Its a cleaner event if a hit just kills the shield + # without damaging the player. + # However, massive damage events should still be able to + # damage the player. This hopefully gives us a happy medium. + max_spillover = SpazFactory.get().max_shield_spillover_damage + if self.shield_hitpoints <= 0: + + # FIXME: Transition out perhaps? + self.shield.delete() + self.shield = None + SpazFactory.get().shield_down_sound.play(1.0, position=self.node.position) + + # Emit some cool looking sparks when the shield dies. + npos = self.node.position + bs.emitfx(position=(npos[0], npos[1] + 0.9, npos[2]), + velocity=self.node.velocity, + count=random.randrange(20, 30), + scale=1.0, + spread=0.6, + chunk_type='spark') + + else: + SpazFactory.get().shield_hit_sound.play(0.5, position=self.node.position) + + # Emit some cool looking sparks on shield hit. + assert msg.force_direction is not None + bs.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 1.0, + msg.force_direction[1] * 1.0, + msg.force_direction[2] * 1.0), + count=min(30, 5 + int(damage * 0.005)), + scale=0.5, + spread=0.3, + chunk_type='spark') + + # If they passed our spillover threshold, + # pass damage along to spaz. + if self.shield_hitpoints <= -max_spillover: + leftover_damage = -max_spillover - self.shield_hitpoints + shield_leftover_ratio = leftover_damage / damage + + # Scale down the magnitudes applied to spaz accordingly. + mag *= shield_leftover_ratio + velocity_mag *= shield_leftover_ratio + else: + return True # Good job shield! + else: + shield_leftover_ratio = 1.0 + + if msg.flat_damage: + damage = int(msg.flat_damage * self.impact_scale * + shield_leftover_ratio) + else: + # Hit it with an impulse and get the resulting damage. + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], mag, + velocity_mag, msg.radius, 0, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + + damage = int(damage_scale * self.node.damage) + self.node.handlemessage('hurt_sound') + + # Play punch impact sound based on damage if it was a punch. + if msg.hit_type == 'punch': + self.on_punched(damage) + + # If damage was significant, lets show it. + # if damage > 350: + # assert msg.force_direction is not None + # babase.show_damage_count('-' + str(int(damage / 10)) + '%', + # msg.pos, msg.force_direction) + + # Let's always add in a super-punch sound with boxing + # gloves just to differentiate them. + if msg.hit_subtype == 'super_punch': + SpazFactory.get().punch_sound_stronger.play(1.0, position=self.node.position) + if damage > 500: + sounds = SpazFactory.get().punch_sound_strong + sound = sounds[random.randrange(len(sounds))] + else: + sound = SpazFactory.get().punch_sound + sound.play(1.0, position=self.node.position) + + # Throw up some chunks. + assert msg.force_direction is not None + bs.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 0.5, + msg.force_direction[1] * 0.5, + msg.force_direction[2] * 0.5), + count=min(10, 1 + int(damage * 0.0025)), + scale=0.3, + spread=0.03) + + bs.emitfx(position=msg.pos, + chunk_type='sweat', + velocity=(msg.force_direction[0] * 1.3, + msg.force_direction[1] * 1.3 + 5.0, + msg.force_direction[2] * 1.3), + count=min(30, 1 + int(damage * 0.04)), + scale=0.9, + spread=0.28) + + # Momentary flash. + hurtiness = damage * 0.003 + punchpos = (msg.pos[0] + msg.force_direction[0] * 0.02, + msg.pos[1] + msg.force_direction[1] * 0.02, + msg.pos[2] + msg.force_direction[2] * 0.02) + flash_color = (1.0, 0.8, 0.4) + light = bs.newnode( + 'light', + attrs={ + 'position': punchpos, + 'radius': 0.12 + hurtiness * 0.12, + 'intensity': 0.3 * (1.0 + 1.0 * hurtiness), + 'height_attenuated': False, + 'color': flash_color + }) + bs.timer(0.06, light.delete) + + flash = bs.newnode('flash', + attrs={ + 'position': punchpos, + 'size': 0.17 + 0.17 * hurtiness, + 'color': flash_color + }) + bs.timer(0.06, flash.delete) + + if msg.hit_type == 'impact': + assert msg.force_direction is not None + bs.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 2.0, + msg.force_direction[1] * 2.0, + msg.force_direction[2] * 2.0), + count=min(10, 1 + int(damage * 0.01)), + scale=0.4, + spread=0.1) + if self.hitpoints > 0: + + # It's kinda crappy to die from impacts, so lets reduce + # impact damage by a reasonable amount *if* it'll keep us alive + if msg.hit_type == 'impact' and damage > self.hitpoints: + # Drop damage to whatever puts us at 10 hit points, + # or 200 less than it used to be whichever is greater + # (so it *can* still kill us if its high enough) + newdamage = max(damage - 200, self.hitpoints - 10) + damage = newdamage + self.node.handlemessage('flash') + + # If we're holding something, drop it. + if damage > 0.0 and self.node.hold_node: + self.node.hold_node = None + # self.hitpoints -= damage + self.multiplyer += min(damage / 2000, 0.15) + if damage/2000 > 0.05: + self.set_score_text(str(int((self.multiplyer-1)*100))+'%') + # self.node.hurt = 1.0 - float( + # self.hitpoints) / self.hitpoints_max + self.node.hurt = 0.0 + + # If we're cursed, *any* damage blows us up. + if self._cursed and damage > 0: + bs.timer( + 0.05, + bs.WeakCall(self.curse_explode, + msg.get_source_player(bs.Player))) + + # If we're frozen, shatter.. otherwise die if we hit zero + # if self.frozen and (damage > 200 or self.hitpoints <= 0): + # self.shatter() + # elif self.hitpoints <= 0: + # self.node.handlemessage( + # bs.DieMessage(how=babase.DeathType.IMPACT)) + + # If we're dead, take a look at the smoothed damage value + # (which gives us a smoothed average of recent damage) and shatter + # us if its grown high enough. + # if self.hitpoints <= 0: + # damage_avg = self.node.damage_smoothed * damage_scale + # if damage_avg > 1000: + # self.shatter() + + source_player = msg.get_source_player(type(self._player)) + if source_player: + self.last_player_attacked_by = source_player + self.last_attacked_time = bs.time() + self.last_attacked_type = (msg.hit_type, msg.hit_subtype) + Spaz.handlemessage(self, bs.HitMessage) # Augment standard behavior. + activity = self._activity() + if activity is not None and self._player.exists(): + activity.handlemessage(PlayerSpazHurtMessage(self)) + + elif isinstance(msg, bs.DieMessage): + self.oob_effect() + super().handlemessage(msg) + elif isinstance(msg, bs.PowerupMessage): + if msg.poweruptype == 'health': + if self.multiplyer > 2: + self.multiplyer *= 0.5 + else: + self.multiplyer *= 0.75 + self.multiplyer = max(1, self.multiplyer) + self.set_score_text(str(int((self.multiplyer-1)*100))+"%") + super().handlemessage(msg) + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class SuperSmash(bs.TeamGameActivity[Player, Team]): + + name = 'Super Smash' + description = 'Knock everyone off the map.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Boxing Gloves', default=False), + bs.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + maps = bs.app.classic.getmaps('melee') + for m in ['Lake Frigid', 'Hockey Stadium', 'Football Stadium']: + # remove maps without bounds + maps.remove(m) + return maps + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: int | None = None + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + self._boxing_gloves = bool(settings['Boxing Gloves']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.SURVIVAL) + + def get_instance_description(self) -> str | Sequence: + return 'Knock everyone off the map.' + + def get_instance_description_short(self) -> str | Sequence: + return 'Knock off the map.' + + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops(enable_tnt=False) + self._pow = None + self._tnt_drop_timer = bs.timer(1.0 * 0.30, + bs.WeakCall(self._drop_pow_box), + repeat=True) + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + def _drop_pow_box(self) -> None: + if self._pow is not None and self._pow: + return + if len(self.map.tnt_points) == 0: + return + pos = random.choice(self.map.tnt_points) + pos = (pos[0], pos[1] + 1, pos[2]) + self._pow = PowBox(position=pos, velocity=(0.0, 1.0, 0.0)) + + def spawn_player(self, player: Player) -> bs.Actor: + if isinstance(self.session, bs.DualTeamSession): + position = self.map.get_start_position(player.team.id) + else: + # otherwise do free-for-all spawn locations + position = self.map.get_ffa_start_position(self.players) + angle = None + + name = player.getname() + light_color = _math.normalized_color(player.color) + display_color = babase.safecolor(player.color, target_intensity=0.75) + + spaz = SSPlayerSpaz(color=player.color, + highlight=player.highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + # If this is co-op and we're on Courtyard or Runaround, add the + # material that allows us to collide with the player-walls. + # FIXME: Need to generalize this. + if isinstance(self.session, bs.CoopSession) and self.map.getname() in [ + 'Courtyard', 'Tower D' + ]: + mat = self.map.preloaddata['collide_with_wall_material'] + assert isinstance(spaz.node.materials, tuple) + assert isinstance(spaz.node.roller_materials, tuple) + spaz.node.materials += (mat, ) + spaz.node.roller_materials += (mat, ) + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + if self._boxing_gloves: + spaz.equip_boxing_gloves() + + return spaz + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, bs.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + self._dingsound.play() + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + self._dingsound.play() + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, SSPlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + +class Player2(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.lives = 0 + self.icons: List[Icon] = [] + + +class Team2(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: Optional[int] = None + self.spawn_order: List[Player] = [] + + +# ba_meta export bascenev1.GameActivity +class SuperSmashElimination(bs.TeamGameActivity[Player2, Team2]): + + name = 'Super Smash Elimination' + description = 'Knock everyone off the map.' + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, + none_is_winner=True) + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives (0 = Unlimited)', + min_value=0, + default=3, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Boxing Gloves', default=False), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + maps = bs.app.classic.getmaps('melee') + for m in ['Lake Frigid', 'Hockey Stadium', 'Football Stadium']: + # remove maps without bounds + maps.remove(m) + return maps + + def __init__(self, settings: dict): + super().__init__(settings) + self.lives = int(settings['Lives (0 = Unlimited)']) + self.time_limit_only = (self.lives == 0) + if self.time_limit_only: + settings['Time Limit'] = max(60, settings['Time Limit']) + + self._epic_mode = bool(settings['Epic Mode']) + self._time_limit = float(settings['Time Limit']) + + self._start_time: Optional[float] = 1.0 + + self._boxing_gloves = bool(settings['Boxing Gloves']) + self._solo_mode = bool(settings.get('Solo Mode', False)) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.SURVIVAL) + + def get_instance_description(self) -> str | Sequence: + return 'Knock everyone off the map.' + + def get_instance_description_short(self) -> str | Sequence: + return 'Knock off the map.' + + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops(enable_tnt=False) + self._pow = None + self._tnt_drop_timer = bs.timer(1.0 * 0.30, + bs.WeakCall(self._drop_pow_box), + repeat=True) + self._update_icons() + bs.timer(1.0, self.check_end_game, repeat=True) + + def _drop_pow_box(self) -> None: + if self._pow is not None and self._pow: + return + if len(self.map.tnt_points) == 0: + return + pos = random.choice(self.map.tnt_points) + pos = (pos[0], pos[1] + 1, pos[2]) + self._pow = PowBox(position=pos, velocity=(0.0, 1.0, 0.0)) + + def on_player_join(self, player: Player) -> None: + + if self.has_begun(): + if (all(teammate.lives == 0 for teammate in player.team.players) + and player.team.survival_seconds is None): + player.team.survival_seconds = 0 + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + return + + player.lives = self.lives + # create our icon and spawn + player.icons = [Icon(player, + position=(0.0, 50), + scale=0.8)] + if player.lives > 0 or self.time_limit_only: + self.spawn_player(player) + + # dont waste time doing this until begin + if self.has_begun(): + self._update_icons() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + player.icons = None + + # update icons in a moment since our team + # will be gone from the list then + bs.timer(0.0, self._update_icons) + bs.timer(0.1, self.check_end_game, repeat=True) + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) > 1: + print('WTF have', len(team.players), 'players in ffa team') + elif len(team.players) == 1: + player = team.players[0] + if len(player.icons) != 1: + print( + 'WTF have', + len(player.icons), + 'icons in non-solo elim') + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon(player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False)) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + if len(player.icons) != 1: + print( + 'WTF have', + len(player.icons), + 'icons in non-solo elim') + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> bs.Actor: + if isinstance(self.session, bs.DualTeamSession): + position = self.map.get_start_position(player.team.id) + else: + # otherwise do free-for-all spawn locations + position = self.map.get_ffa_start_position(self.players) + angle = None + + name = player.getname() + light_color = _math.normalized_color(player.color) + display_color = babase.safecolor(player.color, target_intensity=0.75) + + spaz = SSPlayerSpaz(color=player.color, + highlight=player.highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + # If this is co-op and we're on Courtyard or Runaround, add the + # material that allows us to collide with the player-walls. + # FIXME: Need to generalize this. + if isinstance(self.session, bs.CoopSession) and self.map.getname() in [ + 'Courtyard', 'Tower D' + ]: + mat = self.map.preloaddata['collide_with_wall_material'] + assert isinstance(spaz.node.materials, tuple) + assert isinstance(spaz.node.roller_materials, tuple) + spaz.node.materials += (mat, ) + spaz.node.roller_materials += (mat, ) + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + + if self._boxing_gloves: + spaz.equip_boxing_gloves() + + return spaz + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + player.lives = 0 + + # if we have any icons, update their state + for icon in player.icons: + icon.handle_player_died() + + # play big death sound on our last death + # or for every one in solo mode + if player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # if we hit zero lives we're dead and the game might be over + if player.lives == 0 and not self.time_limit_only: + # If the whole team is now dead, mark their survival time. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - + self._start_time) + # we still have lives; yay! + else: + self.respawn_player(player) + + bs.timer(0.1, self.check_end_game, repeat=True) + + else: + return super().handlemessage(msg) + return None + + def check_end_game(self) -> None: + if len(self._get_living_teams()) < 2: + bs.timer(0.5, self.end_game) + + def _get_living_teams(self) -> List[Team]: + return [ + team for team in self.teams + if len(team.players) > 0 and any(player.lives > 0 + for player in team.players) + ] + + def end_game(self) -> None: + if self.has_ended(): + return + results = bs.GameResults() + self._vs_text = None # Kill our 'vs' if its there. + for team in self.teams: + results.set_team_score(team, team.survival_seconds) + self.end(results=results) diff --git a/plugins/minigames/the_spaz_game.py b/plugins/minigames/the_spaz_game.py new file mode 100644 index 000000000..74857c18c --- /dev/null +++ b/plugins/minigames/the_spaz_game.py @@ -0,0 +1,127 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) + +# ba_meta require api 8 +""" +TheSpazGame - Mini game where all characters looks identical , identify enemies and kill them. +Author: Mr.Smoothy +Discord: https://discord.gg/ucyaesh +Youtube: https://www.youtube.com/c/HeySmoothy +Website: https://bombsquad-community.web.app +Github: https://github.com/bombsquad-community +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.game.elimination import EliminationGame, Player +from bascenev1lib.actor.spazfactory import SpazFactory +import random + +if TYPE_CHECKING: + from typing import Any, Sequence + + +CHARACTER = 'Spaz' + +# ba_meta export bascenev1.GameActivity + + +class TheSpazGame(EliminationGame): + name = 'TheSpazGame' + description = 'Enemy Spaz AmongUs. Kill them all' + scoreconfig = bs.ScoreConfig( + label='Survived', scoretype=bs.ScoreType.SECONDS, none_is_winner=True + ) + + announce_player_deaths = False + + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.15) + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Lives', default=False) + ) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps('melee') + + def get_instance_description(self) -> str | Sequence: + return ( + 'Enemy Spaz AmongUs. Kill them all' + ) + + def get_instance_description_short(self) -> str | Sequence: + return ( + 'Enemy Spaz AmongUs. Kill them all' + ) + + def __init__(self, settings: dict): + super().__init__(settings) + self._solo_mode = False + + def spawn_player(self, player: Player) -> bs.Actor: + p = [-6, -4.3, -2.6, -0.9, 0.8, 2.5, 4.2, 5.9] + q = [-4, -2.3, -0.6, 1.1, 2.8, 4.5] + + x = random.randrange(0, len(p)) + y = random.randrange(0, len(q)) + spaz = self.spawn_player_spaz(player, position=(p[x], 1.8, q[y])) + spaz.node.color = (1, 1, 1) + spaz.node.highlight = (1, 0.4, 1) + self.update_appearance(spaz, character=CHARACTER) + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + def update_appearance(self, spaz, character): + factory = SpazFactory.get() + media = factory.get_media(character) + for field, value in media.items(): + setattr(spaz.node, field, value) + spaz.node.style = factory.get_style(character) + spaz.node.name = '' diff --git a/plugins/minigames/ufo_fight.py b/plugins/minigames/ufo_fight.py new file mode 100644 index 000000000..0d99f63e0 --- /dev/null +++ b/plugins/minigames/ufo_fight.py @@ -0,0 +1,983 @@ +"""UFO Boss Fight v2.0: +Made by Cross Joy""" + +# Anyone who wanna help me in giving suggestion/ fix bugs/ by creating PR, +# Can visit my github https://github.com/CrossJoy/Bombsquad-Modding + +# You can contact me through discord: +# My Discord Id: Cross Joy#0721 +# My BS Discord Server: https://discford.gg/JyBY6haARJ + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +# --------------------------------------- +# Update v2.0 + +# updated to api 8 +# --------------------------------------- + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.spaz import Spaz +from bascenev1lib.actor.bomb import Blast, Bomb +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.actor.spazbot import SpazBotSet, StickyBot +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Union, Callable + + +class UFODiedMessage: + ufo: UFO + """The UFO that was killed.""" + + killerplayer: bs.Player | None + """The bs.Player that killed it (or None).""" + + how: bs.DeathType + """The particular type of death.""" + + def __init__( + self, + ufo: UFO, + killerplayer: bs.Player | None, + how: bs.DeathType, + ): + """Instantiate with given values.""" + self.spazbot = ufo + self.killerplayer = killerplayer + self.how = how + + +class RoboBot(StickyBot): + character = 'B-9000' + default_bomb_type = 'land_mine' + color = (0, 0, 0) + highlight = (3, 3, 3) + + +class UFO(bs.Actor): + """ + New AI for Boss + """ + + # pylint: disable=too-many-public-methods + # pylint: disable=too-many-locals + + node: bs.Node + + def __init__(self, hitpoints: int = 5000): + + super().__init__() + shared = SharedObjects.get() + + self.update_callback: Callable[[UFO], Any] | None = None + activity = self.activity + assert isinstance(activity, bs.GameActivity) + + self.platform_material = bs.Material() + self.platform_material.add_actions( + conditions=('they_have_material', shared.footing_material), + actions=( + 'modify_part_collision', 'collide', True)) + self.ice_material = bs.Material() + self.ice_material.add_actions( + actions=('modify_part_collision', 'friction', 0.0)) + + self._player_pts: list[tuple[bs.Vec3, bs.Vec3]] | None = None + self._ufo_update_timer: bs.Timer | None = None + self.last_player_attacked_by: bs.Player | None = None + self.last_attacked_time = 0.0 + self.last_attacked_type: tuple[str, str] | None = None + + self.to_target: bs.Vec3 = bs.Vec3(0, 0, 0) + self.dist = (0, 0, 0) + + self._bots = SpazBotSet() + self.frozen = False + self.bot_count = 3 + + self.hitpoints = hitpoints + self.hitpoints_max = hitpoints + self._width = 240 + self._width_max = 240 + self._height = 35 + self._bar_width = 240 + self._bar_height = 35 + self._bar_tex = self._backing_tex = bs.gettexture('bar') + self._cover_tex = bs.gettexture('uiAtlas') + self._mesh = bs.getmesh('meterTransparent') + self.bar_posx = -120 + + self._last_hit_time: int | None = None + self.impact_scale = 1.0 + self._num_times_hit = 0 + + self._sucker_mat = bs.Material() + + self.ufo_material = bs.Material() + self.ufo_material.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=(('modify_node_collision', 'collide', True), + ('modify_part_collision', 'physical', True))) + + self.ufo_material.add_actions( + conditions=(('they_have_material', + shared.object_material), 'or', + ('they_have_material', + shared.footing_material), 'or', + ('they_have_material', + self.ufo_material)), + actions=('modify_part_collision', 'physical', False)) + + activity = bs.get_foreground_host_activity() + point = activity.map.get_flag_position(None) + boss_spawn_pos = (point[0], point[1] + 1, point[2]) + + self.node = bs.newnode('prop', delegate=self, attrs={ + 'position': boss_spawn_pos, + 'velocity': (2, 0, 0), + 'color_texture': bs.gettexture('achievementFootballShutout'), + 'mesh': bs.getmesh('landMine'), + # 'light_mesh': bs.getmesh('powerupSimple'), + 'mesh_scale': 3.3, + 'body': 'landMine', + 'body_scale': 3.3, + 'gravity_scale': 0.2, + 'density': 1, + 'reflection': 'soft', + 'reflection_scale': [0.25], + 'shadow_size': 0.1, + 'max_speed': 1.5, + 'is_area_of_interest': + True, + 'materials': [shared.footing_material, shared.object_material]}) + + self.holder = bs.newnode('region', attrs={ + 'position': ( + boss_spawn_pos[0], boss_spawn_pos[1] - 0.25, + boss_spawn_pos[2]), + 'scale': [6, 0.1, 2.5 - 0.1], + 'type': 'box', + 'materials': (self.platform_material, self.ice_material, + shared.object_material)}) + + self.suck_anim = bs.newnode('locator', + owner=self.node, + attrs={'shape': 'circleOutline', + 'position': ( + boss_spawn_pos[0], + boss_spawn_pos[1] - 0.25, + boss_spawn_pos[2]), + 'color': (4, 4, 4), + 'opacity': 1.0, + 'draw_beauty': True, + 'additive': True}) + + def suck_anim(): + bs.animate_array(self.suck_anim, 'position', 3, + {0: ( + self.node.position[0], + self.node.position[1] - 5, + self.node.position[2]), + 0.5: ( + self.node.position[ + 0] + self.to_target.x / 2, + self.node.position[ + 1] + self.to_target.y / 2, + self.node.position[ + 2] + self.to_target.z / 2)}) + + self.suck_timer = bs.Timer(0.5, suck_anim, repeat=True) + + self.blocks = [] + + self._sucker_mat.add_actions( + conditions=( + ('they_have_material', shared.player_material) + ), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._levitate) + + )) + + # self.sucker = bs.newnode('region', attrs={ + # 'position': ( + # boss_spawn_pos[0], boss_spawn_pos[1] - 2, boss_spawn_pos[2]), + # 'scale': [2, 10, 2], + # 'type': 'box', + # 'materials': self._sucker_mat, }) + + self.suck = bs.newnode('region', + attrs={'position': ( + boss_spawn_pos[0], boss_spawn_pos[1] - 2, + boss_spawn_pos[2]), + 'scale': [1, 10, 1], + 'type': 'box', + 'materials': [self._sucker_mat]}) + + self.node.connectattr('position', self.holder, 'position') + self.node.connectattr('position', self.suck, 'position') + + bs.animate(self.node, 'mesh_scale', { + 0: 0, + 0.2: self.node.mesh_scale * 1.1, + 0.26: self.node.mesh_scale}) + + self.shield_deco = bs.newnode('shield', owner=self.node, + attrs={'color': (4, 4, 4), + 'radius': 1.2}) + self.node.connectattr('position', self.shield_deco, 'position') + self._scoreboard() + self._update() + self.drop_bomb_timer = bs.Timer(1.5, bs.Call(self._drop_bomb), + repeat=True) + + self.drop_bots_timer = bs.Timer(15.0, bs.Call(self._drop_bots), repeat=True) + + def _drop_bots(self) -> None: + p = self.node.position + for i in range(self.bot_count): + bs.timer( + 1.0 + i, + lambda: self._bots.spawn_bot( + RoboBot, pos=(self.node.position[0], + self.node.position[1] - 1, + self.node.position[2]), spawn_time=0.0 + ), + ) + + def _drop_bomb(self) -> None: + t = self.to_target + p = self.node.position + if abs(self.dist[0]) < 2 and abs(self.dist[2]) < 2: + Bomb(position=(p[0], p[1] - 0.5, p[2]), + velocity=(t[0] * 5, 0, t[2] * 5), + bomb_type='land_mine').autoretain().arm() + elif self.hitpoints > self.hitpoints_max * 3 / 4: + Bomb(position=(p[0], p[1] - 1.5, p[2]), + velocity=(t[0] * 8, 2, t[2] * 8), + bomb_type='normal').autoretain() + elif self.hitpoints > self.hitpoints_max * 1 / 2: + Bomb(position=(p[0], p[1] - 1.5, p[2]), + velocity=(t[0] * 8, 2, t[2] * 8), + bomb_type='ice').autoretain() + + elif self.hitpoints > self.hitpoints_max * 1 / 4: + Bomb(position=(p[0], p[1] - 1.5, p[2]), + velocity=(t[0] * 15, 2, t[2] * 15), + bomb_type='sticky').autoretain() + else: + Bomb(position=(p[0], p[1] - 1.5, p[2]), + velocity=(t[0] * 15, 2, t[2] * 15), + bomb_type='impact').autoretain() + + def _levitate(self): + node = bs.getcollision().opposingnode + if node.exists(): + p = node.getdelegate(Spaz, True) + + def raise_player(player: bs.Player): + if player.is_alive(): + node = player.node + try: + node.handlemessage("impulse", node.position[0], + node.position[1] + .5, + node.position[2], 0, 5, 0, 3, 10, 0, + 0, 0, 5, 0) + + except: + pass + + if not self.frozen: + for i in range(7): + bs.timer(0.05 + i / 20, bs.Call(raise_player, p)) + + def on_punched(self, damage: int) -> None: + """Called when this spaz gets punched.""" + + def do_damage(self, msg: Any) -> None: + if not self.node: + return None + + damage = abs(msg.magnitude) + if msg.hit_type == 'explosion': + damage /= 20 + + self.hitpoints -= int(damage) + if self.hitpoints <= 0: + self.handlemessage(bs.DieMessage()) + + def _get_target_player_pt(self) -> tuple[ + bs.Vec3 | None, bs.Vec3 | None]: + """Returns the position and velocity of our target. + + Both values will be None in the case of no target. + """ + assert self.node + botpt = bs.Vec3(self.node.position) + closest_dist: float | None = None + closest_vel: bs.Vec3 | None = None + closest: bs.Vec3 | None = None + assert self._player_pts is not None + for plpt, plvel in self._player_pts: + dist = (plpt - botpt).length() + + # Ignore player-points that are significantly below the bot + # (keeps bots from following players off cliffs). + if (closest_dist is None or dist < closest_dist) and ( + plpt[1] > botpt[1] - 5.0 + ): + closest_dist = dist + closest_vel = plvel + closest = plpt + if closest_dist is not None: + assert closest_vel is not None + assert closest is not None + return ( + bs.Vec3(closest[0], closest[1], closest[2]), + bs.Vec3(closest_vel[0], closest_vel[1], closest_vel[2]), + ) + return None, None + + def set_player_points(self, pts: list[tuple[bs.Vec3, bs.Vec3]]) -> None: + """Provide the spaz-bot with the locations of its enemies.""" + self._player_pts = pts + + def exists(self) -> bool: + return bool(self.node) + + def show_damage_count(self, damage: str, position: Sequence[float], + direction: Sequence[float]) -> None: + """Pop up a damage count at a position in space. + + Category: Gameplay Functions + """ + lifespan = 1.0 + app = bs.app + + # FIXME: Should never vary game elements based on local config. + # (connected clients may have differing configs so they won't + # get the intended results). + do_big = app.ui.uiscale is bs.UIScale.SMALL or app.vr_mode + txtnode = bs.newnode('text', + attrs={ + 'text': damage, + 'in_world': True, + 'h_align': 'center', + 'flatness': 1.0, + 'shadow': 1.0 if do_big else 0.7, + 'color': (1, 0.25, 0.25, 1), + 'scale': 0.035 if do_big else 0.03 + }) + # Translate upward. + tcombine = bs.newnode('combine', owner=txtnode, attrs={'size': 3}) + tcombine.connectattr('output', txtnode, 'position') + v_vals = [] + pval = 0.0 + vval = 0.07 + count = 6 + for i in range(count): + v_vals.append((float(i) / count, pval)) + pval += vval + vval *= 0.5 + p_start = position[0] + p_dir = direction[0] + bs.animate(tcombine, 'input0', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + p_start = position[1] + p_dir = direction[1] + bs.animate(tcombine, 'input1', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + p_start = position[2] + p_dir = direction[2] + bs.animate(tcombine, 'input2', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + bs.animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0}) + bs.timer(lifespan, txtnode.delete) + + def _scoreboard(self) -> None: + self._backing = bs.NodeActor( + bs.newnode('image', + attrs={ + 'position': (self.bar_posx + self._width / 2, -100), + 'scale': (self._width, self._height), + 'opacity': 0.7, + 'color': (0.3, + 0.3, + 0.3), + 'vr_depth': -3, + 'attach': 'topCenter', + 'texture': self._backing_tex + })) + self._bar = bs.NodeActor( + bs.newnode('image', + attrs={ + 'opacity': 1.0, + 'color': (0.5, 0.5, 0.5), + 'attach': 'topCenter', + 'texture': self._bar_tex + })) + self._bar_scale = bs.newnode('combine', + owner=self._bar.node, + attrs={ + 'size': 2, + 'input0': self._bar_width, + 'input1': self._bar_height + }) + self._bar_scale.connectattr('output', self._bar.node, 'scale') + self._bar_position = bs.newnode( + 'combine', + owner=self._bar.node, + attrs={ + 'size': 2, + 'input0': self.bar_posx + self._bar_width / 2, + 'input1': -100 + }) + self._bar_position.connectattr('output', self._bar.node, 'position') + self._cover = bs.NodeActor( + bs.newnode('image', + attrs={ + 'position': (self.bar_posx + 120, -100), + 'scale': + (self._width * 1.15, self._height * 1.6), + 'opacity': 1.0, + 'color': (0.3, + 0.3, + 0.3), + 'vr_depth': 2, + 'attach': 'topCenter', + 'texture': self._cover_tex, + 'mesh_transparent': self._mesh + })) + self._score_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'position': (self.bar_posx + 120, -100), + 'h_attach': 'center', + 'v_attach': 'top', + 'h_align': 'center', + 'v_align': 'center', + 'maxwidth': 130, + 'scale': 0.9, + 'text': '', + 'shadow': 0.5, + 'flatness': 1.0, + 'color': (1, 1, 1, 0.8) + })) + + def _update(self) -> None: + self._score_text.node.text = str(self.hitpoints) + self._bar_width = self.hitpoints * self._width_max / self.hitpoints_max + cur_width = self._bar_scale.input0 + bs.animate(self._bar_scale, 'input0', { + 0.0: cur_width, + 0.1: self._bar_width + }) + cur_x = self._bar_position.input0 + + bs.animate(self._bar_position, 'input0', { + 0.0: cur_x, + 0.1: self.bar_posx + self._bar_width / 2 + }) + + if self.hitpoints > self.hitpoints_max * 3 / 4: + bs.animate_array(self.shield_deco, 'color', 3, + {0: self.shield_deco.color, 0.2: (4, 4, 4)}) + elif self.hitpoints > self.hitpoints_max * 1 / 2: + bs.animate_array(self.shield_deco, 'color', 3, + {0: self.shield_deco.color, 0.2: (3, 3, 5)}) + self.bot_count = 4 + + elif self.hitpoints > self.hitpoints_max * 1 / 4: + bs.animate_array(self.shield_deco, 'color', 3, + {0: self.shield_deco.color, 0.2: (1, 5, 1)}) + self.bot_count = 5 + + else: + bs.animate_array(self.shield_deco, 'color', 3, + {0: self.shield_deco.color, 0.2: (5, 0.2, 0.2)}) + self.bot_count = 6 + + def update_ai(self) -> None: + """Should be called periodically to update the spaz' AI.""" + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + if self.update_callback is not None: + if self.update_callback(self): + # Bot has been handled. + return + + if not self.node: + return + + pos = self.node.position + our_pos = bs.Vec3(pos[0], pos[1] - 3, pos[2]) + + target_pt_raw: bs.Vec3 | None + target_vel: bs.Vec3 | None + + target_pt_raw, target_vel = self._get_target_player_pt() + + try: + dist_raw = (target_pt_raw - our_pos).length() + + target_pt = ( + target_pt_raw + target_vel * dist_raw * 0.3 + ) + except: + return + diff = target_pt - our_pos + # self.dist = diff.length() + self.dist = diff + self.to_target = diff.normalized() + + # p = spaz.node.position + # pt = self.getTargetPosition(p) + # pn = self.node.position + # d = [pt[0] - pn[0], pt[1] - pn[1], pt[2] - pn[2]] + # speed = self.getMaxSpeedByDir(d) + # self.node.velocity = (self.to_target.x, self.to_target.y, self.to_target.z) + if self.hitpoints == 0: + setattr(self.node, 'velocity', + (0, self.to_target.y, 0)) + setattr(self.node, 'extra_acceleration', + (0, self.to_target.y * 80 + 70, + 0)) + elif not self.frozen: + setattr(self.node, 'velocity', + (self.to_target.x, self.to_target.y, self.to_target.z)) + setattr(self.node, 'extra_acceleration', + (self.to_target.x, self.to_target.y * 80 + 70, + self.to_target.z)) + + def on_expire(self) -> None: + super().on_expire() + + # We're being torn down; release our callback(s) so there's + # no chance of them keeping activities or other things alive. + self.update_callback = None + + def animate_mesh(self) -> None: + if not self.node: + return None + # bs.animate(self.node, 'mesh_scale', { + # 0: self.node.mesh_scale, + # 0.08: self.node.mesh_scale * 0.9, + # 0.15: self.node.mesh_scale}) + bs.emitfx(position=self.node.position, + velocity=self.node.velocity, + count=int(6 + random.random() * 10), + scale=0.5, + spread=0.4, + chunk_type='metal') + + def handlemessage(self, msg: Any) -> Any: + # pylint: disable=too-many-branches + assert not self.expired + + if isinstance(msg, bs.HitMessage): + # Don't die on punches (that's annoying). + self.animate_mesh() + if self.hitpoints != 0: + self.do_damage(msg) + # self.show_damage_msg(msg) + self._update() + + elif isinstance(msg, bs.DieMessage): + if self.node: + self.hitpoints = 0 + self.frozen = True + self.suck_timer = False + self.drop_bomb_timer = False + self.drop_bots_timer = False + + p = self.node.position + + for i in range(6): + def ded_explode(count): + p_x = p[0] + random.uniform(-1, 1) + p_z = p[2] + random.uniform(-1, 1) + if count == 5: + Blast( + position=(p[0], p[1], p[2]), + blast_type='tnt', + blast_radius=5.0).autoretain() + else: + Blast( + position=(p_x, p[1], p_z), + blast_radius=2.0).autoretain() + + bs.timer(0 + i, bs.Call(ded_explode, i)) + + bs.timer(5, self.node.delete) + bs.timer(0.1, self.suck.delete) + bs.timer(0.1, self.suck_anim.delete) + + elif isinstance(msg, bs.OutOfBoundsMessage): + activity = bs.get_foreground_host_activity() + try: + point = activity.map.get_flag_position(None) + boss_spawn_pos = (point[0], point[1] + 1.5, point[2]) + assert self.node + self.node.position = boss_spawn_pos + except: + self.handlemessage(bs.DieMessage()) + + elif isinstance(msg, bs.FreezeMessage): + if not self.frozen: + self.frozen = True + self.drop_bomb_timer = False + self.drop_bots_timer = False + setattr(self.node, 'velocity', + (0, self.to_target.y, 0)) + setattr(self.node, 'extra_acceleration', + (0, 0, 0)) + self.node.reflection_scale = [2] + + def unfrozen(): + self.frozen = False + self.drop_bomb_timer = bs.Timer(1.5, + bs.Call(self._drop_bomb), + repeat=True) + + self.drop_bots_timer = bs.Timer(15.0, + bs.Call(self._drop_bots), + repeat=True) + self.node.reflection_scale = [0.25] + + bs.timer(3.0, unfrozen) + + else: + super().handlemessage(msg) + + +class UFOSet: + """A container/controller for one or more bs.SpazBots. + + category: Bot Classes + """ + + def __init__(self) -> None: + """Create a bot-set.""" + + # We spread our bots out over a few lists so we can update + # them in a staggered fashion. + self._ufo_bot_list_count = 5 + self._ufo_bot_add_list = 0 + self._ufo_bot_update_list = 0 + self._ufo_bot_lists: list[list[UFO]] = [ + [] for _ in range(self._ufo_bot_list_count) + ] + self._ufo_spawn_sound = bs.getsound('spawn') + self._ufo_spawning_count = 0 + self._ufo_bot_update_timer: bs.Timer | None = None + self.start_moving() + + def _update(self) -> None: + + # Update one of our bot lists each time through. + # First off, remove no-longer-existing bots from the list. + try: + bot_list = self._ufo_bot_lists[self._ufo_bot_update_list] = [ + b for b in self._ufo_bot_lists[self._ufo_bot_update_list] if b + ] + except Exception: + bot_list = [] + bs.print_exception( + 'Error updating bot list: ' + + str(self._ufo_bot_lists[self._ufo_bot_update_list]) + ) + self._bot_update_list = ( + self._ufo_bot_update_list + 1 + ) % self._ufo_bot_list_count + + # Update our list of player points for the bots to use. + player_pts = [] + for player in bs.getactivity().players: + assert isinstance(player, bs.Player) + try: + # TODO: could use abstracted player.position here so we + # don't have to assume their actor type, but we have no + # abstracted velocity as of yet. + if player.is_alive(): + assert isinstance(player.actor, UFO) + assert player.actor.node + player_pts.append( + ( + bs.Vec3(player.actor.node.position), + bs.Vec3(player.actor.node.velocity), + ) + ) + except Exception: + bs.print_exception('Error on bot-set _update.') + + for bot in bot_list: + bot.set_player_points(player_pts) + bot.update_ai() + + def start_moving(self) -> None: + """Start processing bot AI updates so they start doing their thing.""" + self._ufo_bot_update_timer = bs.Timer( + 0.05, bs.WeakCall(self._update), repeat=True + ) + + def spawn_bot( + self, + bot_type: type[UFO], + pos: Sequence[float], + spawn_time: float = 3.0, + on_spawn_call: Callable[[UFO], Any] | None = None, + ) -> None: + """Spawn a bot from this set.""" + from bascenev1lib.actor import spawner + + spawner.Spawner( + pt=pos, + spawn_time=spawn_time, + send_spawn_message=False, + spawn_callback=bs.Call( + self._spawn_bot, bot_type, pos, on_spawn_call + ), + ) + self._ufo_spawning_count += 1 + + def _spawn_bot( + self, + bot_type: type[UFO], + pos: Sequence[float], + on_spawn_call: Callable[[UFO], Any] | None, + ) -> None: + spaz = bot_type() + self._ufo_spawn_sound.play(position=pos) + assert spaz.node + spaz.node.handlemessage('flash') + spaz.node.is_area_of_interest = False + spaz.handlemessage(bs.StandMessage(pos, random.uniform(0, 360))) + self.add_bot(spaz) + self._ufo_spawning_count -= 1 + if on_spawn_call is not None: + on_spawn_call(spaz) + + def add_bot(self, bot: UFO) -> None: + """Add a bs.SpazBot instance to the set.""" + self._ufo_bot_lists[self._ufo_bot_add_list].append(bot) + self._ufo_bot_add_list = ( + self._ufo_bot_add_list + 1) % self._ufo_bot_list_count + + def have_living_bots(self) -> bool: + """Return whether any bots in the set are alive or spawning.""" + return self._ufo_spawning_count > 0 or any( + any(b.is_alive() for b in l) for l in self._ufo_bot_lists + ) + + def get_living_bots(self) -> list[UFO]: + """Get the living bots in the set.""" + bots: list[UFO] = [] + for botlist in self._ufo_bot_lists: + for bot in botlist: + if bot.is_alive(): + bots.append(bot) + return bots + + def clear(self) -> None: + """Immediately clear out any bots in the set.""" + + # Don't do this if the activity is shutting down or dead. + activity = bs.getactivity(doraise=False) + if activity is None or activity.expired: + return + + for i, bot_list in enumerate(self._ufo_bot_lists): + for bot in bot_list: + bot.handlemessage(bs.DieMessage(immediate=True)) + self._ufo_bot_lists[i] = [] + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class UFOightGame(bs.TeamGameActivity[Player, Team]): + """ + A co-op game where you try to defeat UFO Boss + as fast as possible + """ + + name = 'UFO Fight' + description = 'REal Boss Fight?' + scoreconfig = bs.ScoreConfig( + label='Time', scoretype=bs.ScoreType.MILLISECONDS, lower_is_better=True + ) + default_music = bs.MusicType.TO_THE_DEATH + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + # For now we're hard-coding spawn positions and whatnot + # so we need to be sure to specify that we only support + # a specific map. + return ['Football Stadium'] + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + # We currently support Co-Op only. + return issubclass(sessiontype, bs.CoopSession) + + # In the constructor we should load any media we need/etc. + # ...but not actually create anything yet. + def __init__(self, settings: dict): + super().__init__(settings) + self._winsound = bs.getsound('score') + self._won = False + self._timer: OnScreenTimer | None = None + self._bots = UFOSet() + self._preset = str(settings['preset']) + self._credit = bs.newnode('text', + attrs={ + 'v_attach': 'bottom', + 'h_align': 'center', + 'color': (0.4, 0.4, 0.4), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, 20), + 'scale': 0.7, + 'text': 'By Cross Joy' + }) + + def on_transition_in(self) -> None: + super().on_transition_in() + gnode = bs.getactivity().globalsnode + gnode.tint = (0.42, 0.55, 0.66) + + # Called when our game actually begins. + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_powerup_drops() + + # In pro mode there's no powerups. + + # Make our on-screen timer and start it roughly when our bots appear. + self._timer = OnScreenTimer() + bs.timer(4.0, self._timer.start) + + def checker(): + if not self._won: + self.timer = bs.Timer(0.1, self._check_if_won, repeat=True) + + bs.timer(10, checker) + activity = bs.get_foreground_host_activity() + + point = activity.map.get_flag_position(None) + boss_spawn_pos = (point[0], point[1] + 1.5, point[2]) + + # Spawn some baddies. + bs.timer( + 1.0, + lambda: self._bots.spawn_bot( + UFO, pos=boss_spawn_pos, spawn_time=3.0 + ), + ) + + # Called for each spawning player. + + def _check_if_won(self) -> None: + # Simply end the game if there's no living bots. + # FIXME: Should also make sure all bots have been spawned; + # if spawning is spread out enough that we're able to kill + # all living bots before the next spawns, it would incorrectly + # count as a win. + if not self._bots.have_living_bots(): + self.timer = False + self._won = True + self.end_game() + + # Called for miscellaneous messages. + def handlemessage(self, msg: Any) -> Any: + + # A player has died. + if isinstance(msg, bs.PlayerDiedMessage): + player = msg.getplayer(Player) + self.stats.player_was_killed(player) + bs.timer(0.1, self._checkroundover) + + # A spaz-bot has died. + elif isinstance(msg, UFODiedMessage): + # Unfortunately the ufo will always tell us there are living + # bots if we ask here (the currently-dying bot isn't officially + # marked dead yet) ..so lets push a call into the event loop to + # check once this guy has finished dying. + bs.pushcall(self._check_if_won) + + # Let the base class handle anything we don't. + else: + return super().handlemessage(msg) + return None + + # When this is called, we should fill out results and end the game + # *regardless* of whether is has been won. (this may be called due + # to a tournament ending or other external reason). + + def _checkroundover(self) -> None: + """End the round if conditions are met.""" + if not any(player.is_alive() for player in self.teams[0].players): + self.end_game() + + def end_game(self) -> None: + + # Stop our on-screen timer so players can see what they got. + assert self._timer is not None + self._timer.stop() + + results = bs.GameResults() + + # If we won, set our score to the elapsed time in milliseconds. + # (there should just be 1 team here since this is co-op). + # ..if we didn't win, leave scores as default (None) which means + # we lost. + if self._won: + elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0) + bs.cameraflash() + self._winsound.play() + for team in self.teams: + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage()) + results.set_team_score(team, elapsed_time_ms) + + # Ends the activity. + self.end(results) + + +# ba_meta export plugin +class MyUFOFightLevel(babase.Plugin): + + def on_app_running(self) -> None: + babase.app.classic.add_coop_practice_level( + bs.Level( + name='The UFO Fight', + displayname='${GAME}', + gametype=UFOightGame, + settings={'preset': 'regular'}, + preview_texture_name='footballStadiumPreview', + ) + ) diff --git a/plugins/minigames/ultimate_last_stand.py b/plugins/minigames/ultimate_last_stand.py new file mode 100644 index 000000000..67f36cf3a --- /dev/null +++ b/plugins/minigames/ultimate_last_stand.py @@ -0,0 +1,623 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +"""Ultimate Last Stand V2: +Made by Cross Joy""" + +# Anyone who wanna help me in giving suggestion/ fix bugs/ by creating PR, +# Can visit my github https://github.com/CrossJoy/Bombsquad-Modding + +# You can contact me through discord: +# My Discord Id: Cross Joy#0721 +# My BS Discord Server: https://discord.gg/JyBY6haARJ + + +# ---------------------------------------------------------------------------- +# V2 What's new? + +# - The "Player can't fight each other" system is removed, +# players exploiting the features and, I know ideas how to fix it especially +# the freeze handlemessage + +# - Added new bot: Ice Bot + +# - The bot spawn location will be more randomize rather than based on players +# position, I don't wanna players stay at the corner of the map. + +# - Some codes clean up. + +# ---------------------------------------------------------------------------- + +# ba_meta require api 8 + +from __future__ import annotations + +import random +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.bomb import TNTSpawner +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.spazbot import (SpazBot, SpazBotSet, BomberBot, + BomberBotPro, BomberBotProShielded, + BrawlerBot, BrawlerBotPro, + BrawlerBotProShielded, TriggerBot, + TriggerBotPro, TriggerBotProShielded, + ChargerBot, StickyBot, ExplodeyBot) + +if TYPE_CHECKING: + from typing import Any, Sequence + from bascenev1lib.actor.spazbot import SpazBot + + +class IceBot(SpazBot): + """A slow moving bot with ice bombs. + + category: Bot Classes + """ + character = 'Pascal' + punchiness = 0.9 + throwiness = 1 + charge_speed_min = 1 + charge_speed_max = 1 + throw_dist_min = 5.0 + throw_dist_max = 20 + run = True + charge_dist_min = 10.0 + charge_dist_max = 11.0 + default_bomb_type = 'ice' + default_bomb_count = 1 + points_mult = 3 + + +class Icon(bs.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: tuple[float, float], + scale: float, + show_lives: bool = True, + show_death: bool = True, + name_scale: float = 1.0, + name_maxwidth: float = 115.0, + flatness: float = 1.0, + shadow: float = 1.0): + super().__init__() + + self._player = player + self._show_lives = show_lives + self._show_death = show_death + self._name_scale = name_scale + self._outline_tex = bs.gettexture('characterIconMask') + + icon = player.get_icon() + self.node = bs.newnode('image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + self._name_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': flatness, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + if self._show_lives: + self._lives_text = bs.newnode('text', + owner=self.node, + attrs={ + 'text': 'x0', + 'color': (1, 1, 0.5), + 'h_align': 'left', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_position_and_scale(position, scale) + + def set_position_and_scale(self, position: tuple[float, float], + scale: float) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + if self._show_lives: + self._lives_text.position = (position[0] + scale * 10.0, + position[1] - scale * 43.0) + self._lives_text.scale = 1.0 * scale + + def update_for_lives(self) -> None: + """Update for the target player's current lives.""" + if self._player: + lives = self._player.lives + else: + lives = 0 + if self._show_lives: + if lives > 0: + self._lives_text.text = 'x' + str(lives - 1) + else: + self._lives_text.text = '' + if lives == 0: + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + def handle_player_spawned(self) -> None: + """Our player spawned; hooray!""" + if not self.node: + return + self.node.opacity = 1.0 + self.update_for_lives() + + def handle_player_died(self) -> None: + """Well poo; our player died.""" + if not self.node: + return + if self._show_death: + bs.animate( + self.node, 'opacity', { + 0.00: 1.0, + 0.05: 0.0, + 0.10: 1.0, + 0.15: 0.0, + 0.20: 1.0, + 0.25: 0.0, + 0.30: 1.0, + 0.35: 0.0, + 0.40: 1.0, + 0.45: 0.0, + 0.50: 1.0, + 0.55: 0.2 + }) + lives = self._player.lives + if lives == 0: + bs.timer(0.6, self.update_for_lives) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + self.node.delete() + return None + return super().handlemessage(msg) + + +@dataclass +class SpawnInfo: + """Spawning info for a particular bot type.""" + spawnrate: float + increase: float + dincrease: float + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: float | None = None + self.lives = 0 + self.icons: list[Icon] = [] + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: int | None = None + self.spawn_order: list[Player] = [] + + +# ba_meta export bascenev1.GameActivity +class UltimateLastStand(bs.TeamGameActivity[Player, Team]): + """Minigame involving dodging falling bombs.""" + + name = 'Ultimate Last Stand' + description = 'Only the strongest will stand at the end.' + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, + none_is_winner=True) + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + + # Don't allow joining after we start + # (would enable leave/rejoin tomfoolery). + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, + sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append( + bs.BoolSetting('Balance Total Lives', default=False)) + return settings + + # We're currently hard-coded for one map. + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Rampage'] + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + + self._scoreboard = Scoreboard() + self._start_time: float | None = None + self._vs_text: bs.Actor | None = None + self._round_end_timer: bs.Timer | None = None + self._lives_per_player = int(settings['Lives Per Player']) + self._balance_total_lives = bool( + settings.get('Balance Total Lives', False)) + self._epic_mode = settings.get('Epic Mode', True) + self._last_player_death_time: float | None = None + self._timer: OnScreenTimer | None = None + self._tntspawner: TNTSpawner | None = None + self._new_wave_sound = bs.getsound('scoreHit01') + self._bots = SpazBotSet() + self._tntspawnpos = (0, 5.5, -6) + self.spazList = [] + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + + self.node = bs.newnode('text', + attrs={ + 'v_attach': 'bottom', + 'h_align': 'center', + 'color': (0.83, 0.69, 0.21), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, 75), + 'scale': 0.7, + 'text': 'By Cross Joy' + }) + + # For each bot type: [spawnrate, increase, d_increase] + self._bot_spawn_types = { + BomberBot: SpawnInfo(1.00, 0.00, 0.000), + BomberBotPro: SpawnInfo(0.00, 0.05, 0.001), + BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002), + BrawlerBot: SpawnInfo(1.00, 0.00, 0.000), + BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001), + BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), + TriggerBot: SpawnInfo(0.30, 0.00, 0.000), + TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001), + TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), + ChargerBot: SpawnInfo(0.30, 0.05, 0.000), + StickyBot: SpawnInfo(0.10, 0.03, 0.001), + IceBot: SpawnInfo(0.10, 0.03, 0.001), + ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002) + } # yapf: disable + + # Some base class overrides: + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + if self._epic_mode: + self.slow_motion = True + + def get_instance_description(self) -> str | Sequence: + return 'Only the strongest team will stand at the end.' if isinstance( + self.session, + bs.DualTeamSession) else 'Only the strongest will stand at the end.' + + def get_instance_description_short(self) -> str | Sequence: + return 'Only the strongest team will stand at the end.' if isinstance( + self.session, + bs.DualTeamSession) else 'Only the strongest will stand at the end.' + + def on_transition_in(self) -> None: + super().on_transition_in() + bs.timer(1.3, self._new_wave_sound.play) + + def on_player_join(self, player: Player) -> None: + player.lives = self._lives_per_player + + # Don't waste time doing this until begin. + player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: + self.spawn_player(player) + + if self.has_begun(): + self._update_icons() + + def on_begin(self) -> None: + super().on_begin() + bs.animate_array(node=self.node, attr='color', size=3, keys={ + 0.0: (0.5, 0.5, 0.5), + 0.8: (0.83, 0.69, 0.21), + 1.6: (0.5, 0.5, 0.5) + }, loop=True) + + bs.timer(0.001, bs.WeakCall(self._start_bot_updates)) + self._tntspawner = TNTSpawner(position=self._tntspawnpos, + respawn_time=10.0) + + self._timer = OnScreenTimer() + self._timer.start() + self.setup_standard_powerup_drops() + + # Check for immediate end (if we've only got 1 player, etc). + self._start_time = bs.time() + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, bs.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + bs.timer(1.0, self._update, repeat=True) + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def on_player_leave(self, player: Player) -> None: + # Augment default behavior. + super().on_player_leave(player) + player.icons = [] + + # Update icons in a moment since our team will be gone from the + # list then. + bs.timer(0, self._update_icons) + + # If the player to leave was the last in spawn order and had + # their final turn currently in-progress, mark the survival time + # for their team. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - self._start_time) + + # A departing player may trigger game-over. + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> bs.Actor: + actor = self.spawn_player_spaz(player) + bs.timer(0.3, babase.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + return actor + + def _print_lives(self, player: Player) -> None: + from bascenev1lib.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + popuptext.PopupText('x' + str(player.lives - 1), + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=player.node.position).autoretain() + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + def _start_bot_updates(self) -> None: + self._bot_update_interval = 3.3 - 0.3 * (len(self.players)) + self._update_bots() + self._update_bots() + if len(self.players) > 2: + self._update_bots() + if len(self.players) > 3: + self._update_bots() + self._bot_update_timer = bs.Timer(self._bot_update_interval, + bs.WeakCall(self._update_bots)) + + def _update_bots(self) -> None: + assert self._bot_update_interval is not None + self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98) + self._bot_update_timer = bs.Timer(self._bot_update_interval, + bs.WeakCall(self._update_bots)) + botspawnpts: list[Sequence[float]] = [[-5.0, 5.5, -4.14], + [0.0, 5.5, -4.14], + [5.0, 5.5, -4.14]] + for player in self.players: + try: + if player.is_alive(): + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + except Exception: + babase.print_exception('Error updating bots.') + + spawnpt = random.choice( + [botspawnpts[0], botspawnpts[1], botspawnpts[2]]) + + spawnpt = (spawnpt[0] + 3.0 * (random.random() - 0.5), spawnpt[1], + 2.0 * (random.random() - 0.5) + spawnpt[2]) + + # Normalize our bot type total and find a random number within that. + total = 0.0 + for spawninfo in self._bot_spawn_types.values(): + total += spawninfo.spawnrate + randval = random.random() * total + + # Now go back through and see where this value falls. + total = 0 + bottype: type[SpazBot] | None = None + for spawntype, spawninfo in self._bot_spawn_types.items(): + total += spawninfo.spawnrate + if randval <= total: + bottype = spawntype + break + spawn_time = 1.0 + assert bottype is not None + self._bots.spawn_bot(bottype, pos=spawnpt, spawn_time=spawn_time) + + # After every spawn we adjust our ratios slightly to get more + # difficult. + for spawninfo in self._bot_spawn_types.values(): + spawninfo.spawnrate += spawninfo.increase + spawninfo.increase += spawninfo.dincrease + + # Various high-level game events come through this method. + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = bs.time() + + # Record the player's moment of death. + # assert isinstance(msg.spaz.player + msg.getplayer(Player).death_time = curtime + + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + # If the whole team is now dead, mark their survival time. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - + self._start_time) + else: + # Otherwise, in regular mode, respawn. + self.respawn_player(player) + + def _get_living_teams(self) -> list[Team]: + return [ + team for team in self.teams + if len(team.players) > 0 and any(player.lives > 0 + for player in team.players) + ] + + def _update(self) -> None: + # If we're down to 1 or fewer living teams, start a timer to end + # the game (allows the dust to settle and draws to occur if deaths + # are close enough). + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0.5, self.end_game) + + def end_game(self) -> None: + # Stop updating our time text, and set the final time to match + # exactly when our last guy died. + self._timer.stop(endtime=self._last_player_death_time) + + # Ok now calc game results: set a score for each team and then tell + # the game to end. + results = bs.GameResults() + + # Remember that 'free-for-all' mode is simply a special form + # of 'teams' mode where each player gets their own team, so we can + # just always deal in teams and have all cases covered. + for team in self.teams: + # Submit the score value in milliseconds. + results.set_team_score(team, team.survival_seconds) + + self.end(results=results) diff --git a/plugins/minigames/volleyball.py b/plugins/minigames/volleyball.py new file mode 100644 index 000000000..d375ac752 --- /dev/null +++ b/plugins/minigames/volleyball.py @@ -0,0 +1,762 @@ +# Volley Ball (final) + +# Made by your friend: Freaku + + +# Join BCS: +# https://discord.gg/ucyaesh + + +# My GitHub: +# https://github.com/Freaku17/BombSquad-Mods-byFreaku + + +# CHANGELOG: +""" +## 2021 +- Fixed Puck's mass/size/positions/texture/effects +- Fixed Goal positions +- Better center wall +- Added 1 more map +- Added more customisable options +- Map lights locators are now looped (thus reducing the size of the file and lengthy work...) +- Merged map & minigame in one file +- Puck spawns according to scored team +- Also puck now spawns in airrr +- Server support added :) +- Fixed **LOTS** of errors/bugs + +## 2022 +- Code cleanup +- More accurate Goal positions +""" + + +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import random +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.actor.bomb import BombFactory +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + + +class Puck(bs.Actor): + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.05, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + assert activity is not None + assert isinstance(activity, VolleyBallGame) + pmats = [shared.object_material, activity.puck_material] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': activity.puck_mesh, + 'color_texture': activity.puck_tex, + 'body': 'sphere', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.6, + 'mesh_scale': 0.4, + 'body_scale': 1.07, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + + # Since it rolls on spawn, lets make gravity + # to 0, and when another node (bomb/spaz) + # touches it. It'll act back as our normie puck! + bs.animate(self.node, 'gravity_scale', {0: -0.1, 0.2: 1}, False) + # When other node touches, it realises its new gravity_scale + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class VolleyBallGame(bs.TeamGameActivity[Player, Team]): + name = 'Volley Ball' + description = 'Score some goals.\nby \ue048Freaku' + available_settings = [ + bs.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', True), + bs.BoolSetting('Night Mode', False), + bs.BoolSetting('Icy Floor', True), + bs.BoolSetting('Disable Punch', False), + bs.BoolSetting('Disable Bombs', False), + bs.BoolSetting('Enable Bottom Credits', True), + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Open Field', 'Closed Arena'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = bs.getsound('cheer') + self._chant_sound = bs.getsound('crowdChant') + self._foghorn_sound = bs.getsound('foghorn') + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self.puck_mesh = bs.getmesh('shield') + self.puck_tex = bs.gettexture('gameCircleIcon') + self._puck_sound = bs.getsound('metalHit') + self.puck_material = bs.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.puck_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._puck_sound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', bs.DieMessage()))) + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + + self._wall_material = bs.Material() + self._fake_wall_material = bs.Material() + self._wall_material.add_actions( + + actions=( + ('modify_part_collision', 'friction', 100000), + )) + self._wall_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=( + ('modify_part_collision', 'collide', False), + )) + + self._wall_material.add_actions( + conditions=(('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material)), + actions=( + ('modify_part_collision', 'collide', False), + )) + self._wall_material.add_actions( + conditions=('they_have_material', shared.footing_material), + actions=( + ('modify_part_collision', 'friction', 9999.5), + )) + self._wall_material.add_actions( + conditions=('they_have_material', BombFactory.get().blast_material), + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + + )) + self._fake_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.blocks = [] + + self._net_wall_material = bs.Material() + self._net_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + + self._net_wall_material.add_actions( + conditions=('they_have_material', shared.object_material), + actions=( + ('modify_part_collision', 'collide', True), + )) + self._net_wall_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=( + ('modify_part_collision', 'collide', True), + )) + self._net_wall_material.add_actions( + conditions=('we_are_older_than', 1), + actions=( + ('modify_part_collision', 'collide', True), + )) + self.net_blocc = [] + + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[bs.NodeActor]] = None + self._puck: Optional[Puck] = None + self._score_to_win = int(settings['Score to Win']) + self._punchie_ = bool(settings['Disable Punch']) + self._night_mode = bool(settings['Night Mode']) + self._bombies_ = bool(settings['Disable Bombs']) + self._time_limit = float(settings['Time Limit']) + self._icy_flooor = bool(settings['Icy Floor']) + self.credit_text = bool(settings['Enable Bottom Credits']) + self._epic_mode = bool(settings['Epic Mode']) + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Score a goal.' + return 'Score ${ARG1} goals.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'score a goal' + return 'score ${ARG1} goals', self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + if self._night_mode: + bs.getactivity().globalsnode.tint = (0.5, 0.7, 1) + self._puck_spawn_pos = self.map.get_flag_position(None) + self._spawn_puck() + + # Set up the two score regions. + self._score_regions = [] + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (5.7, 0, -0.065), + 'scale': (10.7, 0.001, 8), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (-5.7, 0, -0.065), + 'scale': (10.7, 0.001, 8), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._update_scoreboard() + self._chant_sound.play() + if self.credit_text: + t = bs.newnode('text', + attrs={'text': "Created by Freaku\nVolleyBall", # Disable 'Enable Bottom Credits' when making playlist, No need to edit this lovely... + 'scale': 0.7, + 'position': (0, 0), + 'shadow': 0.5, + 'flatness': 1.2, + 'color': (1, 1, 1), + 'h_align': 'center', + 'v_attach': 'bottom'}) + shared = SharedObjects.get() + self.blocks.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 2.4, 0), 'scale': ( + 0.8, 6, 20), 'type': 'box', 'materials': (self._fake_wall_material, )}))) + + self.net_blocc.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( + 0.6, 2.4, 20), 'type': 'box', 'materials': (self._net_wall_material, )}))) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = bs.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + puck.last_players_to_touch[player.team.id] = player + + def _kill_puck(self) -> None: + self._puck = None + + def _handle_score(self) -> None: + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = bs.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + # Change puck Spawn + if team.id == 0: # left side scored + self._puck_spawn_pos = (5, 0.42, 0) + elif team.id == 1: # right side scored + self._puck_spawn_pos = (-5, 0.42, 0) + else: # normally shouldn't occur + self._puck_spawn_pos = (0, 0.42, 0) + # Easy pizzy + + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 100, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + self._foghorn_sound.play() + self._cheer_sound.play() + + self._puck.scored = True + + # Kill the puck (it'll respawn itself shortly). + bs.emitfx(position=bs.getcollision().position, count=int( + 6.0 + 7.0 * 12), scale=3, spread=0.5, chunk_type='spark') + bs.timer(0.7, self._kill_puck) + + bs.cameraflash(duration=7.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def on_transition_in(self) -> None: + super().on_transition_in() + activity = bs.getactivity() + if self._icy_flooor: + activity.map.is_hockey = True + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, winscore) + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> bs.Actor: + spaz = self.spawn_player_spaz(player) + + if self._bombies_: + # We want the button to work, just no bombs... + spaz.bomb_count = 0 + # Imagine not being able to swipe those colorful buttons ;( + + if self._punchie_: + spaz.connect_controls_to_player(enable_punch=False) + + return spaz + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + bs.timer(2.2, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + # Effect >>>>>> Flashly + bs.emitfx(position=self._puck_spawn_pos, count=int( + 6.0 + 7.0 * 12), scale=1.7, spread=0.4, chunk_type='spark') + + def _spawn_puck(self) -> None: + self._swipsound.play() + self._whistle_sound.play() + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) + + +class Pointzz: + points, boxes = {}, {} + points['spawn1'] = (-8.03866, 0.02275, 0.0) + (0.5, 0.05, 4.0) + points['spawn2'] = (8.82311, 0.01092, 0.0) + (0.5, 0.05, 4.0) + boxes['area_of_interest_bounds'] = (0.0, 1.18575, 0.43262) + \ + (0, 0, 0) + (29.81803, 11.57249, 18.89134) + boxes['map_bounds'] = (0.0, 1.185751251, 0.4326226188) + (0.0, 0.0, 0.0) + ( + 42.09506485, 22.81173179, 29.76723155) + + +class PointzzforH: + points, boxes = {}, {} + boxes['area_of_interest_bounds'] = (0.0, 0.7956858119, 0.0) + \ + (0.0, 0.0, 0.0) + (30.80223883, 0.5961646365, 13.88431707) + boxes['map_bounds'] = (0.0, 0.7956858119, -0.4689020853) + (0.0, 0.0, 0.0) + ( + 35.16182389, 12.18696164, 21.52869693) + points['spawn1'] = (-6.835352227, 0.02305323209, 0.0) + (1.0, 1.0, 3.0) + points['spawn2'] = (6.857415055, 0.03938567998, 0.0) + (1.0, 1.0, 3.0) + + +class VolleyBallMap(bs.Map): + defs = Pointzz() + name = "Open Field" + + @classmethod + def get_play_types(cls) -> List[str]: + return [] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'footballStadiumPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'mesh': bs.getmesh('footballStadium'), + 'vr_fill_mesh': bs.getmesh('footballStadiumVRFill'), + 'collision_mesh': bs.getcollisionmesh('footballStadiumCollide'), + 'tex': bs.gettexture('footballStadium') + } + return data + + def __init__(self): + super().__init__() + shared = SharedObjects.get() + x = -5 + while x < 5: + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, 0, x), + 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, .25, x), + 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, .5, x), + 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, .75, x), + 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, 1, x), + 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + x = x + 0.5 + + y = -1 + while y > -11: + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (y, 0.01, 4), + 'color': (0, 0, 1), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (y, 0.01, -4), + 'color': (0, 0, 1), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (-y, 0.01, 4), + 'color': (1, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (-y, 0.01, -4), + 'color': (1, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + y -= 1 + + z = 0 + while z < 5: + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (11, 0.01, z), + 'color': (1, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (11, 0.01, -z), + 'color': (1, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (-11, 0.01, z), + 'color': (0, 0, 1), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (-11, 0.01, -z), + 'color': (0, 0, 1), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + z += 1 + + self.node = bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': self.preloaddata['mesh'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['vr_fill_mesh'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['tex'] + }) + gnode = bs.getactivity().globalsnode + gnode.tint = (1.3, 1.2, 1.0) + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + + +class VolleyBallMapH(bs.Map): + defs = PointzzforH() + name = 'Closed Arena' + + @classmethod + def get_play_types(cls) -> List[str]: + return [] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'hockeyStadiumPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'meshs': (bs.getmesh('hockeyStadiumOuter'), + bs.getmesh('hockeyStadiumInner')), + 'vr_fill_mesh': bs.getmesh('footballStadiumVRFill'), + 'collision_mesh': bs.getcollisionmesh('hockeyStadiumCollide'), + 'tex': bs.gettexture('hockeyStadium'), + } + mat = bs.Material() + mat.add_actions(actions=('modify_part_collision', 'friction', 0.01)) + data['ice_material'] = mat + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + x = -5 + while x < 5: + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, 0, x), + 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, .25, x), + 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, .5, x), + 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, .75, x), + 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (0, 1, x), + 'color': (1, 1, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + x = x + 0.5 + + y = -1 + while y > -11: + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (y, 0.01, 4), + 'color': (0, 0, 1), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (y, 0.01, -4), + 'color': (0, 0, 1), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (-y, 0.01, 4), + 'color': (1, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (-y, 0.01, -4), + 'color': (1, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + y -= 1 + + z = 0 + while z < 5: + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (11, 0.01, z), + 'color': (1, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (11, 0.01, -z), + 'color': (1, 0, 0), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (-11, 0.01, z), + 'color': (0, 0, 1), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + self.zone = bs.newnode('locator', attrs={'shape': 'circle', 'position': (-11, 0.01, -z), + 'color': (0, 0, 1), 'opacity': 1, 'draw_beauty': True, 'additive': False, 'size': [0.40]}) + z += 1 + + self.node = bs.newnode('terrain', + delegate=self, + attrs={ + 'mesh': + None, + 'collision_mesh': + # we dont want Goalposts... + bs.getcollisionmesh('footballStadiumCollide'), + 'color_texture': + self.preloaddata['tex'], + 'materials': [ + shared.footing_material] + }) + bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['vr_fill_mesh'], + 'vr_only': True, + 'lighting': False, + 'background': True, + }) + mats = [shared.footing_material] + self.floor = bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['meshs'][1], + 'color_texture': self.preloaddata['tex'], + 'opacity': 0.92, + 'opacity_in_low_or_medium_quality': 1.0, + 'materials': mats, + 'color': (0.4, 0.9, 0) + }) + + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': bs.getmesh('natureBackground'), + 'lighting': False, + 'background': True, + 'color': (0.5, 0.30, 0.4) + }) + + gnode = bs.getactivity().globalsnode + gnode.floor_reflection = True + gnode.debris_friction = 0.3 + gnode.debris_kill_height = -0.3 + gnode.tint = (1.2, 1.3, 1.33) + gnode.ambient_color = (1.15, 1.25, 1.6) + gnode.vignette_outer = (0.66, 0.67, 0.73) + gnode.vignette_inner = (0.93, 0.93, 0.95) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + # self.is_hockey = True + + +bs._map.register_map(VolleyBallMap) +bs._map.register_map(VolleyBallMapH) + + +# ba_meta export babase.Plugin +class byFreaku(babase.Plugin): + def __init__(self): + # Reason of plugin: + # To register maps. + # + # Then why not include function here? + # On server upon first launch, plugins are not activated, + # (same can be case for user if disabled auto-enable plugins) + pass diff --git a/plugins/minigames/yeeting_party.py b/plugins/minigames/yeeting_party.py new file mode 100644 index 000000000..18553635f --- /dev/null +++ b/plugins/minigames/yeeting_party.py @@ -0,0 +1,33 @@ +# Made by your friend: Freaku + + +import babase +import bascenev1 as bs +from bascenev1lib.game.deathmatch import Player, DeathMatchGame + + +# ba_meta require api 8 +# ba_meta export bascenev1.GameActivity +class YeetingGame(DeathMatchGame): + """A game of yeeting people out of map""" + + name = 'Yeeting Party!' + description = 'Yeet your enemies out of the map' + + @classmethod + def get_supported_maps(cls, sessiontype): + return ['Bridgit', 'Rampage', 'Monkey Face'] + + def get_instance_description(self): + return 'Yeet ${ARG1} enemies out of the map!', self._score_to_win + + def get_instance_description_short(self): + return 'yeet ${ARG1} enemies', self._score_to_win + + def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None: + pass + + def spawn_player(self, player: Player): + spaz = self.spawn_player_spaz(player) + spaz.connect_controls_to_player(enable_punch=False, enable_bomb=False) + return spaz diff --git a/plugins/minigames/you_vs_bombsquad.py b/plugins/minigames/you_vs_bombsquad.py new file mode 100644 index 000000000..9fbd7e127 --- /dev/null +++ b/plugins/minigames/you_vs_bombsquad.py @@ -0,0 +1,505 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +"""you vs BombSquad / Created by: byANG3L""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import random +from bascenev1lib.actor.spazbot import SpazBotSet, BrawlerBot, SpazBotDiedMessage +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Optional + +lang = bs.app.lang.language +if lang == 'Spanish': + name = 'Tu vs BombSquad' + name_easy = 'Tu vs BS Fácil' + name_easy_epic = 'Tu vs BS Fácil Épico' + name_hard = 'Tu vs BS Difícil' + name_hard_epic = 'Tu vs BS Difícil Épico' +else: + name = 'You vs BombSquad' + name_easy = 'You vs BS Easy' + name_easy_epic = 'You vs BS Easy Epic' + name_hard = 'You vs BS Hard' + name_hard_epic = 'You vs BS Hard Epic' + + +#### BOTS #### + + +class SpazBot(BrawlerBot): + character = 'Spaz' + color = (0.1, 0.35, 0.1) + highlight = (1, 0.15, 0.15) + + +class ZoeBot(BrawlerBot): + character = 'Zoe' + color = (0.6, 0.6, 0.6) + highlight = (0, 1, 0) + + +class SnakeBot(BrawlerBot): + character = 'Snake Shadow' + color = (1, 1, 1) + highlight = (0.55, 0.8, 0.55) + + +class MelBot(BrawlerBot): + character = 'Mel' + color = (1, 1, 1) + highlight = (0.1, 0.6, 0.1) + + +class JackBot(BrawlerBot): + character = 'Jack Morgan' + color = (1, 0.2, 0.1) + highlight = (1, 1, 0) + + +class SantaBot(BrawlerBot): + character = 'Santa Claus' + color = (1, 0, 0) + highlight = (1, 1, 1) + + +class FrostyBot(BrawlerBot): + character = 'Frosty' + color = (0.5, 0.5, 1) + highlight = (1, 0.5, 0) + + +class BonesBot(BrawlerBot): + character = 'Bones' + color = (0.6, 0.9, 1) + highlight = (0.6, 0.9, 1) + + +class BernardBot(BrawlerBot): + character = 'Bernard' + color = (0.7, 0.5, 0.0) + highlight = (0.6, 0.5, 0.8) + + +class PascalBot(BrawlerBot): + character = 'Pascal' + color = (0.3, 0.5, 0.8) + highlight = (1, 0, 0) + + +class TaobaoBot(BrawlerBot): + character = 'Taobao Mascot' + color = (1, 0.5, 0) + highlight = (1, 1, 1) + + +class BBot(BrawlerBot): + character = 'B-9000' + color = (0.5, 0.5, 0.5) + highlight = (1, 0, 0) + + +class AgentBot(BrawlerBot): + character = 'Agent Johnson' + color = (0.3, 0.3, 0.33) + highlight = (1, 0.5, 0.3) + + +class GrumbledorfBot(BrawlerBot): + character = 'Grumbledorf' + color = (0.2, 0.4, 1.0) + highlight = (0.06, 0.15, 0.4) + + +class PixelBot(BrawlerBot): + character = 'Pixel' + color = (0, 1, 0.7) + highlight = (0.65, 0.35, 0.75) + + +class BunnyBot(BrawlerBot): + character = 'Easter Bunny' + color = (1, 1, 1) + highlight = (1, 0.5, 0.5) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class TUvsBombSquad(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = name + description = 'Defeat all enemies.' + scoreconfig = bs.ScoreConfig(label='Time', + scoretype=bs.ScoreType.MILLISECONDS, + lower_is_better=True) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.BoolSetting('Hard Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.CoopSession) + or issubclass(sessiontype, bs.MultiTeamSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._winsound = bs.getsound('score') + self._won = False + self._timer: Optional[OnScreenTimer] = None + self._bots = SpazBotSet() + self._hard_mode = bool(settings['Hard Mode']) + self._epic_mode = bool(settings['Epic Mode']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.SURVIVAL) + + self._spaz_easy: list = [[(-5.4146, 0.9515, -3.0379), 23.0], + [(-5.4146, 0.9515, 1.0379), 23.0]] + self._spaz_hard: list = [[(11.4146, 0.9515, -5.0379), 3.0], + [(-8.4146, 0.9515, -5.0379), 5.0], + [(5.4146, 0.9515, -3.0379), 8.0], + [(5.4146, 0.9515, 1.0379), 8.0]] + self._zoe_easy: list = [[(5.4146, 0.9515, -1.0379), 23.0], + [(-5.4146, 0.9515, 5.0379), 23.0]] + self._zoe_hard: list = [[(-11.4146, 0.9515, -5.0379), 3.0], + [(8.4146, 0.9515, -3.0379), 5.0], + [(-5.4146, 0.9515, -3.0379), 8.0], + [(-5.4146, 0.9515, 1.0379), 8.0]] + self._snake_easy: list = [[(-5.4146, 0.9515, -1.0379), 23.0], + [(5.4146, 0.9515, -5.0379), 23.0]] + self._snake_hard: list = [[(11.4146, 0.9515, -3.0379), 3.0], + [(-8.4146, 0.9515, -3.0379), 5.0], + [(5.4146, 0.9515, -1.0379), 8.0], + [(5.4146, 0.9515, 1.0379), 8.0]] + self._kronk_easy: list = [[(8.4146, 0.9515, 1.0379), 10.0], + [(5.4146, 0.9515, 3.0379), 23.0]] + self._kronk_hard: list = [[(-11.4146, 0.9515, -3.0379), 3.0], + [(8.4146, 0.9515, -1.0379), 5.0], + [(-5.4146, 0.9515, -1.0379), 8.0], + [(5.4146, 0.9515, 1.0379), 8.0]] + self._mel_easy: list = [[(5.4146, 0.9515, 1.0379), 23.0], + [(-11.4146, 0.9515, 1.0379), 3.0]] + self._mel_hard: list = [[(11.4146, 0.9515, -1.0379), 3.0], + [(-8.4146, 0.9515, -1.0379), 5.0], + [(5.4146, 0.9515, 1.0379), 8.0], + [(5.4146, 0.9515, 5.0379), 8.0]] + self._jack_easy: list = [[(-8.4146, 0.9515, 1.0379), 10.0], + [(5.4146, 0.9515, 1.0379), 23.0]] + self._jack_hard: list = [[(-11.4146, 0.9515, -1.0379), 3.0], + [(8.4146, 0.9515, 1.0379), 5.0], + [(-5.4146, 0.9515, 1.0379), 8.0], + [(-5.4146, 0.9515, 5.0379), 8.0], + [(5.4146, 0.9515, -5.0379), 8.0]] + self._frosty_easy: list = [[(8.4146, 0.9515, 1.0379), 10.0], + [(8.4146, 0.9515, -5.0379), 10.0]] + self._frosty_hard: list = [[(-11.4146, 0.9515, 1.0379), 3.0], + [(-5.4146, 0.9515, 3.0379), 8.0], + [(-5.4146, 0.9515, -5.0379), 8.0], + [(5.4146, 0.9515, 3.0379), 8.0]] + self._bunny_easy: list = [[(-8.4146, 0.9515, 3.0379), 10.0], + [(5.4146, 0.9515, 5.0379), 23.0]] + self._bunny_hard: list = [[(8.4146, 0.9515, -5.0379), 5.0], + [(-5.4146, 0.9515, -5.0379), 8.0], + [(-5.4146, 0.9515, 3.0379), 8.0], + [(8.4146, 0.9515, 3.0379), 5.0]] + self._bones_easy: list = [[(11.4146, 0.9515, -5.0379), 3.0], + [(-8.4146, 0.9515, -5.0379), 10.0]] + self._bones_hard: list = [[(5.4146, 0.9515, -3.0379), 8.0], + [(-5.4146, 0.9515, 3.0379), 8.0], + [(5.4146, 0.9515, 1.0379), 8.0], + [(8.4146, 0.9515, 3.0379), 5.0]] + self._bernard_easy: list = [[(-11.4146, 0.9515, -5.0379), 3.0], + [(8.4146, 0.9515, -3.0379), 10.0]] + self._bernard_hard: list = [[(-5.4146, 0.9515, -3.0379), 8.0], + [(5.4146, 0.9515, 1.0379), 8.0], + [(-5.4146, 0.9515, 1.0379), 8.0], + [(-8.4146, 0.9515, 3.0379), 5.0]] + self._pascal_easy: list = [[(11.4146, 0.9515, -3.0379), 3.0], + [(-8.4146, 0.9515, -3.0379), 10.0]] + self._pascal_hard: list = [[(5.4146, 0.9515, -1.0379), 8.0], + [(-5.4146, 0.9515, 1.0379), 8.0], + [(5.4146, 0.9515, 1.0379), 8.0], + [(8.4146, 0.9515, 1.0379), 5.0]] + self._taobao_easy: list = [[(-11.4146, 0.9515, -3.0379), 3.0], + [(8.4146, 0.9515, -1.0379), 10.0]] + self._taobao_hard: list = [[(-5.4146, 0.9515, -1.0379), 8.0], + [(5.4146, 0.9515, 1.0379), 8.0], + [(-5.4146, 0.9515, 1.0379), 8.0], + [(-5.4146, 0.9515, 1.0379), 8.0]] + self._bbot_easy: list = [[(11.4146, 0.9515, -1.0379), 3.0], + [(-8.4146, 0.9515, -1.0379), 10.0]] + self._bbot_hard: list = [[(-5.4146, 0.9515, 1.0379), 8.0], + [(8.4146, 0.9515, 1.0379), 5.0], + [(-5.4146, 0.9515, 1.0379), 8.0], + [(-5.4146, 0.9515, 1.0379), 8.0]] + self._agent_easy: list = [[(-11.4146, 0.9515, -1.0379), 3.0], + [(8.4146, 0.9515, 1.0379), 10.0]] + self._agent_hard: list = [[(5.4146, 0.9515, 5.0379), 8.0], + [(-8.4146, 0.9515, 1.0379), 5.0], + [(-11.4146, 0.9515, 1.0379), 3.0], + [(-11.4146, 0.9515, 1.0379), 3.0]] + self._wizard_easy: list = [[(11.4146, 0.9515, 1.0379), 3.0], + [(-8.4146, 0.9515, 1.0379), 10.0]] + self._wizard_hard: list = [[(-5.4146, 0.9515, 5.0379), 8.0], + [(8.4146, 0.9515, 5.0379), 5.0], + [(-5.4146, 0.9515, 1.0379), 8.0], + [(11.4146, 0.9515, 1.0379), 3.0]] + self._pixel_easy: list = [[(-5.4146, 0.9515, -5.0379), 23.0]] + self._pixel_hard: list = [[(5.4146, 0.9515, -5.0379), 8.0], + [(5.4146, 0.9515, 3.0379), 5.0], + [(-8.4146, 0.9515, 5.0379), 5.0]] + self._santa_easy: list = [[(-8.4146, 0.9515, 5.0379), 23.0]] + self._santa_hard: list = [[(-8.4146, 0.9515, 1.0379), 5.0], + [(-8.4146, 0.9515, 5.0379), 5.0], + [(5.4146, 0.9515, 1.0379), 8.0]] + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_powerup_drops() + self._timer = OnScreenTimer() + bs.timer(4.0, self._timer.start) + + for i in range(len(self._spaz_easy)): + self._spawn_bots(4.0, SpazBot, + self._spaz_easy[i][0], self._spaz_easy[i][1]) + for i in range(len(self._zoe_easy)): + self._spawn_bots(4.0, ZoeBot, + self._zoe_easy[i][0], self._zoe_easy[i][1]) + for i in range(len(self._snake_easy)): + self._spawn_bots(4.0, SnakeBot, + self._snake_easy[i][0], self._snake_easy[i][1]) + for i in range(len(self._kronk_easy)): + self._spawn_bots(4.0, BrawlerBot, + self._kronk_easy[i][0], self._kronk_easy[i][1]) + for i in range(len(self._mel_easy)): + self._spawn_bots(4.0, MelBot, + self._mel_easy[i][0], self._mel_easy[i][1]) + for i in range(len(self._jack_easy)): + self._spawn_bots(4.0, JackBot, + self._jack_easy[i][0], self._jack_easy[i][1]) + for i in range(len(self._santa_easy)): + self._spawn_bots(4.0, SantaBot, + self._santa_easy[i][0], self._santa_easy[i][1]) + for i in range(len(self._frosty_easy)): + self._spawn_bots(4.0, FrostyBot, + self._frosty_easy[i][0], self._frosty_easy[i][1]) + for i in range(len(self._bunny_easy)): + self._spawn_bots(4.0, BunnyBot, + self._bunny_easy[i][0], self._bunny_easy[i][1]) + for i in range(len(self._bones_easy)): + self._spawn_bots(4.0, BonesBot, + self._bones_easy[i][0], self._bones_easy[i][1]) + for i in range(len(self._bernard_easy)): + self._spawn_bots(4.0, BernardBot, + self._bernard_easy[i][0], self._bernard_easy[i][1]) + for i in range(len(self._pascal_easy)): + self._spawn_bots(4.0, PascalBot, + self._pascal_easy[i][0], self._pascal_easy[i][1]) + for i in range(len(self._taobao_easy)): + self._spawn_bots(4.0, TaobaoBot, + self._taobao_easy[i][0], self._taobao_easy[i][1]) + for i in range(len(self._bbot_easy)): + self._spawn_bots(4.0, BBot, + self._bbot_easy[i][0], self._bbot_easy[i][1]) + for i in range(len(self._agent_easy)): + self._spawn_bots(4.0, AgentBot, + self._agent_easy[i][0], self._agent_easy[i][1]) + for i in range(len(self._wizard_easy)): + self._spawn_bots(4.0, GrumbledorfBot, + self._wizard_easy[i][0], self._wizard_easy[i][1]) + for i in range(len(self._pixel_easy)): + self._spawn_bots(4.0, PixelBot, + self._pixel_easy[i][0], self._pixel_easy[i][1]) + + if self._hard_mode: + for i in range(len(self._spaz_hard)): + self._spawn_bots(4.0, SpazBot, + self._spaz_hard[i][0], self._spaz_hard[i][1]) + for i in range(len(self._zoe_hard)): + self._spawn_bots(4.0, ZoeBot, + self._zoe_hard[i][0], self._zoe_hard[i][1]) + for i in range(len(self._snake_hard)): + self._spawn_bots(4.0, SnakeBot, + self._snake_hard[i][0], self._snake_hard[i][1]) + for i in range(len(self._kronk_hard)): + self._spawn_bots(4.0, BrawlerBot, + self._kronk_hard[i][0], self._kronk_hard[i][1]) + for i in range(len(self._mel_hard)): + self._spawn_bots(4.0, MelBot, + self._mel_hard[i][0], self._mel_hard[i][1]) + for i in range(len(self._jack_hard)): + self._spawn_bots(4.0, JackBot, + self._jack_hard[i][0], self._jack_hard[i][1]) + for i in range(len(self._santa_hard)): + self._spawn_bots(4.0, SantaBot, + self._santa_hard[i][0], self._santa_hard[i][1]) + for i in range(len(self._frosty_hard)): + self._spawn_bots(4.0, FrostyBot, + self._frosty_hard[i][0], self._frosty_hard[i][1]) + for i in range(len(self._bunny_hard)): + self._spawn_bots(4.0, BunnyBot, + self._bunny_hard[i][0], self._bunny_hard[i][1]) + for i in range(len(self._bones_hard)): + self._spawn_bots(4.0, BonesBot, + self._bones_hard[i][0], self._bones_hard[i][1]) + for i in range(len(self._bernard_hard)): + self._spawn_bots(4.0, BernardBot, + self._bernard_hard[i][0], self._bernard_hard[i][1]) + for i in range(len(self._pascal_hard)): + self._spawn_bots(4.0, PascalBot, + self._pascal_hard[i][0], self._pascal_hard[i][1]) + for i in range(len(self._taobao_hard)): + self._spawn_bots(4.0, TaobaoBot, + self._taobao_hard[i][0], self._taobao_hard[i][1]) + for i in range(len(self._bbot_hard)): + self._spawn_bots(4.0, BBot, + self._bbot_hard[i][0], self._bbot_hard[i][1]) + for i in range(len(self._agent_hard)): + self._spawn_bots(4.0, AgentBot, + self._agent_hard[i][0], self._agent_hard[i][1]) + for i in range(len(self._wizard_hard)): + self._spawn_bots(4.0, GrumbledorfBot, + self._wizard_hard[i][0], self._wizard_hard[i][1]) + for i in range(len(self._pixel_hard)): + self._spawn_bots(4.0, PixelBot, + self._pixel_hard[i][0], self._pixel_hard[i][1]) + + def _spawn_bots(self, time: float, bot: Any, + pos: float, spawn_time: float) -> None: + bs.timer(time, lambda: self._bots.spawn_bot( + bot, pos=pos, spawn_time=spawn_time)) + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + return + self.spawn_player(player) + + # Called for each spawning player. + def spawn_player(self, player: Player) -> bs.Actor: + + # Let's spawn close to the center. + spawn_center = (0.0728, 0.0227, -1.9888) + pos = (spawn_center[0] + random.uniform(-0.5, 0.5), spawn_center[1], + spawn_center[2] + random.uniform(-0.5, 0.5)) + return self.spawn_player_spaz(player, position=pos) + + def _check_if_won(self) -> None: + # Simply end the game if there's no living bots. + # FIXME: Should also make sure all bots have been spawned; + # if spawning is spread out enough that we're able to kill + # all living bots before the next spawns, it would incorrectly + # count as a win. + if not self._bots.have_living_bots(): + self._won = True + self.end_game() + + # Called for miscellaneous messages. + def handlemessage(self, msg: Any) -> Any: + + # A player has died. + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard behavior. + self.respawn_player(msg.getplayer(Player)) + + # A spaz-bot has died. + elif isinstance(msg, SpazBotDiedMessage): + # Unfortunately the bot-set will always tell us there are living + # bots if we ask here (the currently-dying bot isn't officially + # marked dead yet) ..so lets push a call into the event loop to + # check once this guy has finished dying. + babase.pushcall(self._check_if_won) + + # Let the base class handle anything we don't. + else: + return super().handlemessage(msg) + return None + + # When this is called, we should fill out results and end the game + # *regardless* of whether is has been won. (this may be called due + # to a tournament ending or other external reason). + def end_game(self) -> None: + + # Stop our on-screen timer so players can see what they got. + assert self._timer is not None + self._timer.stop() + + results = bs.GameResults() + + # If we won, set our score to the elapsed time in milliseconds. + # (there should just be 1 team here since this is co-op). + # ..if we didn't win, leave scores as default (None) which means + # we lost. + if self._won: + elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0) + bs.cameraflash() + self._winsound.play() + for team in self.teams: + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage()) + results.set_team_score(team, elapsed_time_ms) + + # Ends the activity. + self.end(results) + + +# ba_meta export plugin +class plugin(babase.Plugin): + def __init__(self): + ## Campaign support ## + babase.app.classic.add_coop_practice_level(bs.Level( + name=name_easy, + gametype=TUvsBombSquad, + settings={}, + preview_texture_name='footballStadiumPreview')) + babase.app.classic.add_coop_practice_level(bs.Level( + name_easy_epic, + gametype=TUvsBombSquad, + settings={'Epic Mode': True}, + preview_texture_name='footballStadiumPreview')) + babase.app.classic.add_coop_practice_level(bs.Level( + name=name_hard, + gametype=TUvsBombSquad, + settings={'Hard Mode': True}, + preview_texture_name='footballStadiumPreview')) + babase.app.classic.add_coop_practice_level(bs.Level( + name=name_hard_epic, + gametype=TUvsBombSquad, + settings={'Hard Mode': True, + 'Epic Mode': True}, + preview_texture_name='footballStadiumPreview')) diff --git a/plugins/minigames/zombie_horde.py b/plugins/minigames/zombie_horde.py new file mode 100644 index 000000000..bd8bf2b8e --- /dev/null +++ b/plugins/minigames/zombie_horde.py @@ -0,0 +1,885 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import copy +import random +from babase import _math +from bascenev1._coopsession import CoopSession +from bascenev1._messages import PlayerDiedMessage, StandMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.game.elimination import Icon, Player +from bascenev1lib.actor.spaz import PickupMessage +from bascenev1lib.actor.spazbot import SpazBotSet, BrawlerBot, SpazBotDiedMessage +from bascenev1lib.actor.spazfactory import SpazFactory + + +if TYPE_CHECKING: + from typing import Any, Sequence + + +class PlayerSpaz_Zom(PlayerSpaz): + def handlemessage(self, m: Any) -> Any: + if isinstance(m, bs.HitMessage): + if not self.node: + return + if not m._source_player is None: + try: + playa = m._source_player.getname(True, False) + if not playa is None: + if m._source_player.lives < 1: + super().handlemessage(m) + except: + super().handlemessage(m) + else: + super().handlemessage(m) + + elif isinstance(m, bs.FreezeMessage): + pass + + elif isinstance(m, PickupMessage): + if not self.node: + return None + + try: + collision = bs.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + except bs.NotFoundError: + return True + + try: + if opposingnode.invincible: + return True + except Exception: + pass + + try: + playa = opposingnode._source_player.getname(True, False) + if not playa is None: + if opposingnode._source_player.lives > 0: + return True + except Exception: + pass + + if (opposingnode.getnodetype() == 'spaz' + and not opposingnode.shattered and opposingbody == 4): + opposingbody = 1 + + held = self.node.hold_node + if held and held.getnodetype() == 'flag': + return True + + self.node.hold_body = opposingbody + self.node.hold_node = opposingnode + else: + return super().handlemessage(m) + return None + + +class PlayerZombie(PlayerSpaz): + def handlemessage(self, m: Any) -> Any: + if isinstance(m, bs.HitMessage): + if not self.node: + return None + if not m._source_player is None: + try: + playa = m._source_player.getname(True, False) + if playa is None: + pass + else: + super().handlemessage(m) + except: + super().handlemessage(m) + else: + super().handlemessage(m) + else: + super().handlemessage(m) + + +class zBotSet(SpazBotSet): + def start_moving(self) -> None: + """Start processing bot AI updates so they start doing their thing.""" + self._bot_update_timer = bs.Timer(0.05, + bs.WeakCall(self.zUpdate), + repeat=True) + + def zUpdate(self) -> None: + + try: + bot_list = self._bot_lists[self._bot_update_list] = ([ + b for b in self._bot_lists[self._bot_update_list] if b + ]) + except Exception: + bot_list = [] + babase.print_exception('Error updating bot list: ' + + str(self._bot_lists[self._bot_update_list])) + self._bot_update_list = (self._bot_update_list + + 1) % self._bot_list_count + + player_pts = [] + for player in bs.getactivity().players: + assert isinstance(player, bs.Player) + try: + if player.is_alive(): + assert isinstance(player.actor, Spaz) + assert player.actor.node + if player.lives > 0: + player_pts.append( + (babase.Vec3(player.actor.node.position), + babase.Vec3(player.actor.node.velocity))) + except Exception: + babase.print_exception('Error on bot-set _update.') + + for bot in bot_list: + bot.set_player_points(player_pts) + bot.update_ai() + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + self.spawn_order: list[Player] = [] + + +# ba_meta export bascenev1.GameActivity +class ZombieHorde(bs.TeamGameActivity[Player, Team]): + + name = 'Zombie Horde' + description = 'Kill walkers for points!' + scoreconfig = bs.ScoreConfig(label='Score', + scoretype=bs.ScoreType.POINTS, + none_is_winner=False, + lower_is_better=False) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + bs.IntSetting( + 'Max Zombies', + default=10, + min_value=5, + max_value=50, + increment=5, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Lives', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._start_time: float | None = None + self._vs_text: bs.Actor | None = None + self._round_end_timer: bs.Timer | None = None + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = int(settings['Lives Per Player']) + self._max_zombies = int(settings['Max Zombies']) + self._time_limit = float(settings['Time Limit']) + self._balance_total_lives = bool( + settings.get('Balance Total Lives', False)) + self._solo_mode = bool(settings.get('Solo Mode', False)) + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + + self.spazList = [] + self.zombieQ = 0 + + activity = bs.getactivity() + my_factory = SpazFactory.get() + + appears = ['Kronk', 'Zoe', 'Pixel', 'Agent Johnson', + 'Bones', 'Frosty', 'Kronk2'] + myAppear = copy.copy(babase.app.classic.spaz_appearances['Kronk']) + myAppear.name = 'Kronk2' + babase.app.classic.spaz_appearances['Kronk2'] = myAppear + for appear in appears: + my_factory.get_media(appear) + med = my_factory.spaz_media + med['Kronk2']['head_mesh'] = med['Zoe']['head_mesh'] + med['Kronk2']['color_texture'] = med['Agent Johnson']['color_texture'] + med['Kronk2']['color_mask_texture'] = med['Pixel']['color_mask_texture'] + med['Kronk2']['torso_mesh'] = med['Bones']['torso_mesh'] + med['Kronk2']['pelvis_mesh'] = med['Pixel']['pelvis_mesh'] + med['Kronk2']['upper_arm_mesh'] = med['Frosty']['upper_arm_mesh'] + med['Kronk2']['forearm_mesh'] = med['Frosty']['forearm_mesh'] + med['Kronk2']['hand_mesh'] = med['Bones']['hand_mesh'] + med['Kronk2']['upper_leg_mesh'] = med['Bones']['upper_leg_mesh'] + med['Kronk2']['lower_leg_mesh'] = med['Pixel']['lower_leg_mesh'] + med['Kronk2']['toes_mesh'] = med['Bones']['toes_mesh'] + + def get_instance_description(self) -> str | Sequence: + return ('Kill walkers for points! ', + 'Dead player walker: 2 points!') if isinstance( + self.session, bs.DualTeamSession) else ( + 'Kill walkers for points! Dead player walker: 2 points!') + + def get_instance_description_short(self) -> str | Sequence: + return ('Kill walkers for points! ', + 'Dead player walker: 2 points!') if isinstance( + self.session, bs.DualTeamSession) else ( + 'Kill walkers for points! Dead player walker: 2 points!') + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + player.lives = 0 + player.icons = [] + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + return + + player.lives = self._lives_per_player + + if self._solo_mode: + player.icons = [] + player.team.spawn_order.append(player) + self._update_solo_mode() + else: + player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: + self.spawn_player(player) + + if self.has_begun(): + self._update_icons() + + def _update_solo_mode(self) -> None: + for team in self.teams: + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + break + + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self.zombieQ = 1 + if self._solo_mode: + self._vs_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': babase.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, bs.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._bots = zBotSet() + + # Set colors and character for ToughGuyBot to be zombie + setattr(BrawlerBot, 'color', (0.4, 0.1, 0.05)) + setattr(BrawlerBot, 'highlight', (0.2, 0.4, 0.3)) + setattr(BrawlerBot, 'character', 'Kronk2') + # start some timers to spawn bots + thePt = self.map.get_ffa_start_position(self.players) + + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + bs.timer(1.0, self._update, repeat=True) + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon(player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False)) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def _get_spawn_point(self, player: Player) -> babase.Vec3 | None: + del player # Unused. + + # In solo-mode, if there's an existing live player on the map, spawn at + # whichever spot is farthest from them (keeps the action spread out). + if self._solo_mode: + living_player = None + living_player_pos = None + for team in self.teams: + for tplayer in team.players: + if tplayer.is_alive(): + assert tplayer.node + ppos = tplayer.node.position + living_player = tplayer + living_player_pos = ppos + break + if living_player: + assert living_player_pos is not None + player_pos = babase.Vec3(living_player_pos) + points: list[tuple[float, babase.Vec3]] = [] + for team in self.teams: + start_pos = babase.Vec3(self.map.get_start_position(team.id)) + points.append( + ((start_pos - player_pos).length(), start_pos)) + # Hmm.. we need to sorting vectors too? + points.sort(key=lambda x: x[0]) + return points[-1][1] + return None + + def spawn_player(self, player: Player) -> bs.Actor: + position = self.map.get_ffa_start_position(self.players) + angle = 20 + name = player.getname() + + light_color = _math.normalized_color(player.color) + display_color = _babase.safecolor(player.color, target_intensity=0.75) + spaz = PlayerSpaz_Zom(color=player.color, + highlight=player.highlight, + character=player.character, + player=player) + player.actor = spaz + assert spaz.node + self.spazList.append(spaz) + + if isinstance(self.session, CoopSession) and self.map.getname() in [ + 'Courtyard', 'Tower D' + ]: + mat = self.map.preloaddata['collide_with_wall_material'] + assert isinstance(spaz.node.materials, tuple) + assert isinstance(spaz.node.roller_materials, tuple) + spaz.node.materials += (mat, ) + spaz.node.roller_materials += (mat, ) + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + factory = SpazFactory() + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + bs.Sound.play(self._spawn_sound, 1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + for icon in player.icons: + icon.handle_player_spawned() + return spaz + + def respawn_player_zombie(self, + player: Player, + respawn_time: float | None = None) -> None: + # pylint: disable=cyclic-import + + assert player + if respawn_time is None: + teamsize = len(player.team.players) + if teamsize == 1: + respawn_time = 3.0 + elif teamsize == 2: + respawn_time = 5.0 + elif teamsize == 3: + respawn_time = 6.0 + else: + respawn_time = 7.0 + + # If this standard setting is present, factor it in. + if 'Respawn Times' in self.settings_raw: + respawn_time *= self.settings_raw['Respawn Times'] + + # We want whole seconds. + assert respawn_time is not None + respawn_time = round(max(1.0, respawn_time), 0) + + if player.actor and not self.has_ended(): + from bascenev1lib.actor.respawnicon import RespawnIcon + player.customdata['respawn_timer'] = bs.Timer( + respawn_time, bs.WeakCall( + self.spawn_player_if_exists_as_zombie, player)) + player.customdata['respawn_icon'] = RespawnIcon( + player, respawn_time) + + def spawn_player_if_exists_as_zombie(self, player: PlayerT) -> None: + """ + A utility method which calls self.spawn_player() *only* if the + bs.Player provided still exists; handy for use in timers and whatnot. + + There is no need to override this; just override spawn_player(). + """ + if player: + self.spawn_player_zombie(player) + + def spawn_player_zombie(self, player: PlayerT) -> bs.Actor: + position = self.map.get_ffa_start_position(self.players) + angle = 20 + name = player.getname() + + light_color = _math.normalized_color(player.color) + display_color = _babase.safecolor(player.color, target_intensity=0.75) + spaz = PlayerSpaz_Zom(color=player.color, + highlight=player.highlight, + character='Kronk2', + player=player) + player.actor = spaz + assert spaz.node + self.spazList.append(spaz) + + if isinstance(self.session, CoopSession) and self.map.getname() in [ + 'Courtyard', 'Tower D' + ]: + mat = self.map.preloaddata['collide_with_wall_material'] + assert isinstance(spaz.node.materials, tuple) + assert isinstance(spaz.node.roller_materials, tuple) + spaz.node.materials += (mat, ) + spaz.node.roller_materials += (mat, ) + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player(enable_punch=True, + enable_bomb=False, + enable_pickup=False) + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + bs.Sound.play(self._spawn_sound, 1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + for icon in player.icons: + icon.handle_player_spawned() + return spaz + + def _print_lives(self, player: Player) -> None: + from bascenev1lib.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + try: + pos = player.actor.node.position + except Exception as e: + print('EXC getting player pos in bsElim', e) + return + if player.lives > 0: + popuptext.PopupText('x' + str(player.lives - 1), + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=pos).autoretain() + else: + popuptext.PopupText('Dead!', + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=pos).autoretain() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + player.icons = [] + + # Remove us from spawn-order. + if self._solo_mode: + if player in player.team.spawn_order: + player.team.spawn_order.remove(player) + + # Update icons in a moment since our team will be gone from the + # list then. + bs.timer(0, self._update_icons) + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + if player.lives > 0: + player.lives -= 1 + else: + if msg._killerplayer: + if msg._killerplayer.lives > 0: + msg._killerplayer.team.score += 2 + self._update_scoreboard() + + if msg._player in self.spazList: + self.spazList.remove(msg._player) + if player.lives < 0: + babase.print_error( + "Got lives < 0 in Elim; this shouldn't happen. solo:" + + str(self._solo_mode)) + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if self._solo_mode or player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + self.respawn_player_zombie(player) + else: + # Otherwise, in regular mode, respawn. + if not self._solo_mode: + self.respawn_player(player) + + # In solo, put ourself at the back of the spawn order. + if self._solo_mode: + player.team.spawn_order.remove(player) + player.team.spawn_order.append(player) + + elif isinstance(msg, SpazBotDiedMessage): + self._onSpazBotDied(msg) + # bs.PopupText("died",position=self._position,color=popupColor,scale=popupScale).autoRetain() + super().handlemessage(msg) + else: + super().handlemessage(msg) + + def _update(self) -> None: + if self.zombieQ > 0: + self.zombieQ -= 1 + self.spawn_zombie() + if self._solo_mode: + # For both teams, find the first player on the spawn order + # list with lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + self._update_icons() + break + + # If we're down to 1 or fewer living teams, start a timer to end + # the game (allows the dust to settle and draws to occur if deaths + # are close enough). + teamsRemain = self._get_living_teams() + if len(teamsRemain) < 2: + if len(teamsRemain) == 1: + theScores = [] + for team in self.teams: + theScores.append(team.score) + if teamsRemain[0].score < max(theScores): + pass + elif teamsRemain[0].score == max( + theScores) and theScores.count(max(theScores)) > 1: + pass + else: + self._round_end_timer = bs.Timer(0.5, self.end_game) + else: + self._round_end_timer = bs.Timer(0.5, self.end_game) + + def spawn_zombie(self) -> None: + # We need a Z height... + thePt = list(self.get_random_point_in_play()) + thePt2 = self.map.get_ffa_start_position(self.players) + thePt[1] = thePt2[1] + bs.timer(0.1, babase.Call( + self._bots.spawn_bot, BrawlerBot, pos=thePt, spawn_time=1.0)) + + def _onSpazBotDied(self, DeathMsg) -> None: + # Just in case we are over max... + if len(self._bots.get_living_bots()) < self._max_zombies: + self.zombieQ += 1 + + if DeathMsg.killerplayer is None: + pass + else: + player = DeathMsg.killerplayer + if not player: + return + if player.lives < 1: + return + player.team.score += 1 + self.zombieQ += 1 + self._update_scoreboard() + + def get_random_point_in_play(self) -> None: + myMap = self.map.getname() + if myMap == 'Doom Shroom': + while True: + x = random.uniform(-1.0, 1.0) + y = random.uniform(-1.0, 1.0) + if x*x+y*y < 1.0: + break + return ((8.0*x, 8.0, -3.5+5.0*y)) + elif myMap == 'Rampage': + x = random.uniform(-6.0, 7.0) + y = random.uniform(-6.0, -2.5) + return ((x, 8.0, y)) + elif myMap == 'Hockey Stadium': + x = random.uniform(-11.5, 11.5) + y = random.uniform(-4.5, 4.5) + return ((x, 5.0, y)) + elif myMap == 'Courtyard': + x = random.uniform(-4.3, 4.3) + y = random.uniform(-4.4, 0.3) + return ((x, 8.0, y)) + elif myMap == 'Crag Castle': + x = random.uniform(-6.7, 8.0) + y = random.uniform(-6.0, 0.0) + return ((x, 12.0, y)) + elif myMap == 'Big G': + x = random.uniform(-8.7, 8.0) + y = random.uniform(-7.5, 6.5) + return ((x, 8.0, y)) + elif myMap == 'Football Stadium': + x = random.uniform(-12.5, 12.5) + y = random.uniform(-5.0, 5.5) + return ((x, 8.0, y)) + else: + x = random.uniform(-5.0, 5.0) + y = random.uniform(-6.0, 0.0) + return ((x, 8.0, y)) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score) + + def _get_living_teams(self) -> list[Team]: + return [ + team for team in self.teams + if len(team.players) > 0 and any(player.lives > 0 + for player in team.players) + ] + + def end_game(self) -> None: + if self.has_ended(): + return + setattr(BrawlerBot, 'color', (0.6, 0.6, 0.6)) + setattr(BrawlerBot, 'highlight', (0.6, 0.6, 0.6)) + setattr(BrawlerBot, 'character', 'Kronk') + results = bs.GameResults() + self._vs_text = None # Kill our 'vs' if its there. + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + +# ba_meta export bascenev1.GameActivity +class ZombieHordeCoop(ZombieHorde): + + name = 'Zombie Horde' + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Football Stadium'] + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.CoopSession)) + + def _update(self) -> None: + if self.zombieQ > 0: + self.zombieQ -= 1 + self.spawn_zombie() + if self._solo_mode: + # For both teams, find the first player on the spawn order + # list with lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + self._update_icons() + break + + if not any(player.is_alive() for player in self.teams[0].players): + self._round_end_timer = bs.Timer(0.5, self.end_game) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior. + bs.TeamGameActivity.handlemessage(self, msg) + player: Player = msg.getplayer(Player) + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + else: + super().handlemessage(msg) + + +# ba_meta export plugin +class ZombieHordeLevel(babase.Plugin): + def on_app_running(self) -> None: + babase.app.classic.add_coop_practice_level( + bs._level.Level( + 'Zombie Horde', + gametype=ZombieHordeCoop, + settings={}, + preview_texture_name='footballStadiumPreview', + ) + ) diff --git a/plugins/utilities.json b/plugins/utilities.json index b98c05db3..7894e7f9c 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -1,59 +1,2387 @@ { "name": "Utilities", "description": "Utilities", - "plugins_base_url": "http://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/utilities", + "plugins_base_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugins/utilities", "plugins": { + "plugin_editor": { + "description": "An Editor which let's you view and edit plugins inside the game!", + "external_url": "", + "authors": [ + { + "name": "Vishuuu", + "email": "vishal.u338@gmail.com", + "discord": "vishal3308" + } + ], + "versions": { + "1.1.1": { + "api_version": 8, + "commit_sha": "bb6734a", + "released_on": "24-05-2024", + "md5sum": "9d5f8f71a4a91d4b47c9ca15a54f3a9e" + } + } + }, + "party_filter": { + "description": "Adds Filters in the gather->public window.", + "external_url": "", + "authors": [ + { + "name": "Yelllow", + "email": "im.yellow.dev@gmail.com", + "discord": "y.lw" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "f643c74", + "released_on": "21-05-2025", + "md5sum": "372a950c8d9833b412888632a0917d30" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "f5b3db1774924f90381e3e4abb7b6278" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "667397e", + "released_on": "04-01-2025", + "md5sum": "b624ef90abf20588f21eb55291f9747d" + } + } + }, + "sandbox": { + "description": "Spawn, control and bots, change music, add teams, spawn objects, change game values in real time, and much more!", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "1.2.2": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "cf94386635b26f6342850672fdffd204" + }, + "1.2.1": { + "api_version": 9, + "commit_sha": "f1917f7", + "released_on": "08-04-2025", + "md5sum": "e523a7a40024c01e6a7595836237ee8b" + }, + "1.2.0": { + "api_version": 8, + "commit_sha": "3a62e48", + "released_on": "23-01-2025", + "md5sum": "1404399f68a4ad40f42dd4230d378627" + } + } + }, + "updown": { + "description": "Adds UP and DOWN buttons in party window to recall messages in chat.", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "1.1.4": { + "api_version": 9, + "commit_sha": "99611b9", + "released_on": "10-08-2025", + "md5sum": "3c13d6364917c02b9055776253127cab" + }, + "1.1.3": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "50870d82855dfe7051ce2ddd86ce7e76" + }, + "1.1.2": { + "api_version": 9, + "commit_sha": "1277ee0", + "released_on": "22-05-2025", + "md5sum": "95681edb02ee0a3d1bbc899dc8187029" + }, + "1.1.1": { + "api_version": 9, + "commit_sha": "f1917f7", + "released_on": "08-04-2025", + "md5sum": "45925247145c0126f73f895932202822" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "90cbaf7", + "released_on": "22-01-2025", + "md5sum": "b287c940c143bf9c4831477742dbd913" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "333416f", + "released_on": "20-05-2024", + "md5sum": "7c597ef8cf7966a056eb5b34c01678eb" + } + } + }, + "rejoin": { + "description": "Deprecated - Better use Power plugin. Adds a button in pause menu, which rejoins current server once clicked! if didn't work, just click again and again. if still, then party is full.", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "2.0.3": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "a5a7dfa880bfd448da57afac112fb279" + }, + "2.0.2": { + "api_version": 9, + "commit_sha": "2f1d9cd", + "released_on": "25-01-2025", + "md5sum": "5df299bf8a5a8a1c8ee2a8535342f5ab" + }, + "2.0.1": { + "api_version": 9, + "commit_sha": "506451f", + "released_on": "21-01-2025", + "md5sum": "13385447ed5cc2d34396a5f57b0aed67" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "333416f", + "released_on": "20-05-2024", + "md5sum": "a34da341e7f2055a1f57f5262fa07c79" + } + } + }, + "power": { + "description": "With one click. Experimental. Adds a dev console tab with some features I find useful. Power is mainly focused on the multiplayer side. Can be considered a good tool to have around.", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "2.7": { + "api_version": 9, + "commit_sha": "99611b9", + "released_on": "10-08-2025", + "md5sum": "6b7cc1c7f3a5ebffc70d1971032b5349" + } + } + }, + "fileman": { + "description": "Advanced file manager. Adds a button to settings menu. Experimental. Read code to know more.", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "1.0": { + "api_version": 9, + "commit_sha": "99611b9", + "released_on": "10-08-2025", + "md5sum": "87cc93d3213f948cc3524c15c00a78df" + } + } + }, + "plugtools": { + "description": "Live Plugin Action. Beta. Adds a dev console tab for plugin management. For full features, read the first lines in the py file, or look in the source.", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "1.5": { + "api_version": 9, + "commit_sha": "99611b9", + "released_on": "10-08-2025", + "md5sum": "dee61f9ef3ad9faa534133c978a02c53" + } + } + }, + "camera": { + "description": "Say cheese. Adds a button to pause menu. Camera is advanced Camera allows you to change camera position and target with a very easy graphical visualization of how it would look like.", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "1.0": { + "api_version": 9, + "commit_sha": "99611b9", + "released_on": "10-08-2025", + "md5sum": "578e1335a1f9d5b3863598e7234ffa80" + } + } + }, + "finder": { + "description": "Find anyone. Experimental. Useful if you are looking for someone, or just messing around. For full features, either check first lines of py file, or check source. Combine with Power plugin for better control.", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "1.0": { + "api_version": 9, + "commit_sha": "99611b9", + "released_on": "10-08-2025", + "md5sum": "86e036b9361f806ad1cf593794fac825" + } + } + }, + "path": { + "description": "Where it's going to be. Experimental. Path tries to predict the next position of bomb. Path relies on velocity to operate. Optionally pass spaz node (holder) to assist prediction.", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "1.0": { + "api_version": 9, + "commit_sha": "99611b9", + "released_on": "10-08-2025", + "md5sum": "0995280087defbb7c0810cad99530558" + } + } + }, + "replay": { + "description": "Simple replay player. Experimental. Adds a button to pause menu and watch menu. For full features, either read first lines of py file, or check source.", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "3.0": { + "api_version": 9, + "commit_sha": "fd4bcc1", + "released_on": "06-11-2025", + "md5sum": "456b1e24a55a0d08b35246e1c69f5acd" + }, + "1.5": { + "api_version": 9, + "commit_sha": "99611b9", + "released_on": "10-08-2025", + "md5sum": "e97e88170046b4822067f3164ee1dcf5" + } + } + }, + "topmsg": { + "description": "When chat is muted, see new chat messages on top right! (like kill logs)", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "1.1.2": { + "api_version": 9, + "commit_sha": "99611b9", + "released_on": "10-08-2025", + "md5sum": "70980bd0861af06d2345336e350fb672" + }, + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "4cd1c34a7c57e0fa72d4dfab96f54dc7" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bbd7753", + "released_on": "22-01-2025", + "md5sum": "7a30597198881b7a1cee2a220230b934" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "333416f", + "released_on": "20-05-2024", + "md5sum": "74a592e8204e81872752a4a6eff6701e" + } + } + }, + "sorry": { + "description": "Send a random sorry to chat, preventing revenge attempts from teammates you kill by mistake.", + "external_url": "https://brobordd.github.io/byBordd", + "authors": [ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + } + ], + "versions": { + "2.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "2a443c5b120f37355ff8df19d7b1f5d9" + }, + "2.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "1b41b40775ed6fd98a2e3a4c4de4778e" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "333416f", + "released_on": "20-05-2024", + "md5sum": "58fd9fbfc6fbff9f617b5a1763fc6aab" + } + } + }, + "translate": { + "description": "Translate yours/others chat. Just click on the message to translate them. Open Plugin Settings/Double click 'Trans' button to open translation settings. Compatible with other PW mods (like advanced_party_window)", + "external_url": "https://github.com/bombsquad-community/plugin-manager/assets/92618708/5860e44d-0b70-4a3f-a651-208a4452ea38", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "" + } + ], + "versions": { + "1.1.3": { + "api_version": 9, + "commit_sha": "23f90dc", + "released_on": "27-10-2025", + "md5sum": "41f8d6185eda5571fd4c48f634e8d59d" + }, + "1.1.2": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "8f50bf1863f7f7778a11ceba130a6ced" + }, + "1.1.1": { + "api_version": 9, + "commit_sha": "6aa11b7", + "released_on": "30-05-2025", + "md5sum": "caeef32b0c8f4d56b985942cd0e0100f" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "da4f281188f46d0c8e518c57c17144d4" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "fcda5f0", + "released_on": "09-02-2024", + "md5sum": "2efa68f899ff04bf345ed5e8c32886d7" + } + } + }, + "random_join": { + "description": "Come visit the unknown servers around all the world! Plugin designed not to join servers with similar names more frequently than rare ones. Have fun!", + "external_url": "", + "authors": [ + { + "name": "maxick", + "email": "", + "discord": "maxick#9227" + }, + { + "name": "LoupGarou", + "email": "LoupGarou5418@outlook.com", + "discord": "loupgarou_" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "0d6bd1b", + "released_on": "17-07-2025", + "md5sum": "ea9260deb1fbd2b09c756f26434176b8" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "2d12dd560558b87f3f1fd6f134f397b8" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "f81810220b0cc13cc436434014fbe8de" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2454845", + "released_on": "26-12-2022", + "md5sum": "7bac6bfe837ff89e7da10a0ab45691d1" + } + } + }, + "manual_camera": { + "description": "Adjust the games camera angle/position. Open pause menu and press 'Manual Camera' button. Useable in OFFLINE/ONLINE/REPLAYS", + "external_url": "", + "authors": [ + { + "name": "Droopy", + "email": "", + "discord": "droopy25" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "6d58aa06593069aa9fac78bd55b333a1" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "022f879", + "released_on": "23-01-2025", + "md5sum": "78a27e0fce1965ffcfdb055c33d49185" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "c26342d", + "released_on": "05-08-2023", + "md5sum": "8d6716d2fca5848a39909ef6bb87ce35" + } + } + }, + "share_replay": { + "description": "Export replays to mods folder and share them with friends or have a backup", + "external_url": "", + "authors": [ + { + "name": "LoupGarou", + "email": "LoupGarou5418@outlook.com", + "discord": "loupgarou_" + } + ], + "versions": { + "1.4.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "ac26aa8656b6fbbc596b8df31af8a67a" + }, + "1.4.0": { + "api_version": 9, + "commit_sha": "3a62e48", + "released_on": "23-01-2025", + "md5sum": "3676a9d0c709bb646eff826526a1f406" + }, + "1.3.2": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "1cf2d07d15dbacf0a277795f3742c14b" + }, + "1.3.1": { + "api_version": 7, + "commit_sha": "d511c15", + "released_on": "03-08-2023", + "md5sum": "c1dea54adc0136638ff56993a7196519" + }, + "1.3.0": { + "api_version": 7, + "commit_sha": "ec116b3", + "released_on": "20-12-2022", + "md5sum": "dbb9d85a5fb0041631dc12765a257fce" + }, + "1.2.1": { + "api_version": 7, + "commit_sha": "7753b87", + "released_on": "05-12-2022", + "md5sum": "7ab5c7ebd43ebd929866fccb82cd5a45" + }, + "1.2.0": { + "api_version": 7, + "commit_sha": "f07d2f8", + "released_on": "04-12-2022", + "md5sum": "f74e2bb80a62bef20f491377324ffbeb" + }, + "1.1.0": { + "api_version": 7, + "commit_sha": "77f92a5", + "released_on": "19-11-2022", + "md5sum": "07eaf53c99b938e08383d09f1cdd01ab" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "d475487c221d78c7f1278410d5ce229e" + } + } + }, + "custom_death": { + "description": "Characters turn to Bones after death", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "1f7dc0faac2cb3c6e140b40a9f032764" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "04a6d85404f51b3026060d9525c14b09" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "beab6387e86bd842ffc8c857750b510e" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "7b1941512532321eec18140087569215" + } + } + }, + "max_players": { + "description": "Increase the max player limit of 8 players", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "176562b207f332d267d0a4cb9b7d9ff7" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "b52d84a", + "released_on": "21-01-2025", + "md5sum": "cc2d3d06a20f4c9de3f3fa3d7eccf46c" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "80f8fd9e9bd23d33daace0059029378b" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "f5e129f009e9732b3179c7edaf75478e" + } + } + }, + "quick_custom_game": { + "description": "Quckly create a custom game with any gamemode", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "6eb01543b28a9a2c95f873aa92dbe3b2" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "b691e35afb17cd3b5f6fe21c0194567d" + } + } + }, + "colored_bomb_explosion_patches": { + "description": "Creates patches of random color after bomb explosions", + "external_url": "https://youtube.com/c/JoseANG3LYT", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.3": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "1b61f43a9886f270fd16f26488c72e04" + }, + "1.0.2": { + "api_version": 9, + "commit_sha": "37914bb", + "released_on": "21-01-2025", + "md5sum": "c1f8a382f0f64777b5415c82d6913ff2" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "ee8187a63d9e205f0355aa5c21141af2" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "fed7c24", + "released_on": "07-11-2022", + "md5sum": "0c351a0aebed77c3771053cde0c18d7b" + } + } + }, + "chat_cmd": { + "description": "chatcmd for a beutiful game - BombSquad OwO,type /help in chat for more info", + "external_url": "", + "authors": [ + { + "name": "IM_NOT_PRANAV#7874", + "email": "", + "discord": "IM_NOT_PRANAV#7874" + }, + { + "name": "FireFighter1037 ", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "ee666a289e34c7ceca1d64ed977de4ce" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "c84d9a1c9e63e77949134020e1f11834" + } + } + }, + "mood_light": { + "description": "Dynamic lighting in co-op games (adjustable using \"ml\" chat command)", + "external_url": "", + "authors": [ + { + "name": "LoupGarou", + "email": "LoupGarou5418@outlook.com", + "discord": "loupgarou_" + } + ], + "versions": { + "1.3.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "7d021516211e9621507feca0795d580e" + }, + "1.3.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "499ccd84ac8deedef5596dccad2a58b7" + }, + "1.2.3": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "6601d41f60b276d54770c0718158e701" + }, + "1.2.2": { + "api_version": 7, + "commit_sha": "7753b87", + "released_on": "05-12-2022", + "md5sum": "c6e5366b446e265c623e34010d5c40c7" + }, + "1.2.1": { + "api_version": 7, + "commit_sha": "2fda676", + "released_on": "07-11-2022", + "md5sum": "ee8eede4a6c47535ab831abd0111aab0" + }, + "1.2.0": { + "api_version": 7, + "commit_sha": "d838115", + "released_on": "06-10-2022", + "md5sum": "bcb8e605969eef35a210590b410e2d44" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "430da49", + "released_on": "04-10-2022", + "md5sum": "ca3407db8509eda577643c0490789571" + } + } + }, "colorscheme": { "description": "Create custom UI colorschemes!", - "external_url": "http://www.youtube.com/watch?v=qatwWrBAvjc", + "external_url": "https://www.youtube.com/watch?v=G6824StL4eg", + "authors": [ + { + "name": "Rikko", + "email": "rikkolovescats@proton.me", + "discord": "rikkolovescats" + }, + { + "name": "Vishal", + "email": "vishal.u338@gmail.com", + "discord": "vishal3308" + } + ], + "versions": { + "2.2.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "5394b53deae34bcd6efcf10d346afc0a" + }, + "2.2.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "ce3ed59ed6009ced568b94105c10ae42" + }, + "2.1.0": { + "api_version": 8, + "commit_sha": "464eb7c", + "released_on": "09-12-2023", + "md5sum": "517fec3938f31627c1cfd2126f1ee9da" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "0b55bc2", + "released_on": "20-06-2023", + "md5sum": "517fec3938f31627c1cfd2126f1ee9da" + }, + "1.2.3": { + "api_version": 7, + "commit_sha": "7753b87", + "released_on": "05-12-2022", + "md5sum": "659794e1fa1e87cf9768899519994eac" + }, + "1.2.2": { + "api_version": 7, + "commit_sha": "a6d1a43", + "released_on": "17-09-2022", + "md5sum": "6c06e45ad4e37e2208bc3e8e128f1b87" + }, + "1.2.1": { + "api_version": 7, + "commit_sha": "109e61c5a", + "released_on": "29-08-2022", + "md5sum": "970360789f4605132eb5091e30a64642" + }, + "1.2.0": { + "api_version": 7, + "commit_sha": "963a17379", + "released_on": "12-08-2022", + "md5sum": "41084bfec41119ca9df8e6d899cd3cc0" + }, + "1.0.0": { + "api_version": 6, + "commit_sha": "da5032a7", + "released_on": "28-05-2021", + "md5sum": "527acfec13a2d0a6115a52df7abca920" + } + } + }, + "store_event_specials": { + "description": "Exposes event-special store items not normally available for purchase", + "external_url": "", + "authors": [ + { + "name": "Rikko", + "email": "rikkolovescats@proton.me", + "discord": "rikkolovescats" + } + ], + "versions": { + "3.0.0": { + "api_version": 9, + "commit_sha": "5273a71", + "released_on": "18-12-2025", + "md5sum": "d261bb7e71de0fe684c5bd6734a8a66a" + }, + "2.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "6c7f31770def4bb7fc7bb7b6ee1e73c7" + }, + "2.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "48896fb8a1ecfdb1ce382b06db315854" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "c41cc10", + "released_on": "15-07-2023", + "md5sum": "71c3f6386433feebac94ef0b9b6a8bf4" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "63d674cf", + "released_on": "06-08-2022", + "md5sum": "233dfaa7f0e9394d21454f4ffa7d0205" + } + } + }, + "unlock_TowerD": { + "description": "Ability to play Tower-D map in ffa/team games!", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "" + } + ], + "versions": { + "2.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "f577aaa7467a1afc99cf3dd71d95571c" + }, + "2.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "02857d12efabced28a6d980756143aeb" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "48f9302", + "released_on": "28-07-2023", + "md5sum": "31df84f027c98e86336a420aa509e47f" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "6beb8ddf", + "released_on": "30-08-2022", + "md5sum": "aac4edfcaeca1dc2910f97e739d67482" + } + } + }, + "floater": { + "description": "Calls a overpowered floater in any gamemode. Chat /floater to activate!", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "" + } + ], + "versions": { + "2.0.4": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "75691c3ca16ef0c0410cf2c76979d3c2" + }, + "2.0.3": { + "api_version": 9, + "commit_sha": "6bf081a", + "released_on": "08-02-2025", + "md5sum": "e80d0282867e44f27d1efeadd13e1edf" + }, + "2.0.2": { + "api_version": 9, + "commit_sha": "2c66eac", + "released_on": "21-01-2025", + "md5sum": "37a68c15b66a2deca923317db9d4c630" + }, + "2.0.1": { + "api_version": 8, + "commit_sha": "1d45e69", + "released_on": "08-12-2023", + "md5sum": "e5ca160fd0c847697fbf61d53462a7cd" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "48f9302", + "released_on": "28-07-2023", + "md5sum": "ef2dbcac9190a61753e2c3c0f8afc22c" + }, + "1.1.0": { + "api_version": 7, + "commit_sha": "383f774", + "released_on": "17-09-2022", + "md5sum": "d20cdf514aeaf8f3db08eb8c5c7867e6" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "858030b", + "released_on": "01-09-2022", + "md5sum": "c024a0774f2e960dad7f633efdc3feb5" + } + } + }, + "easy_connect": { + "description": "Can connect easily to servers by retrying automatically till you join the server.", + "external_url": "", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "3.0.2": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "e88433a885662c4db4d67cc1cb49a5ab" + }, + "3.0.1": { + "api_version": 9, + "commit_sha": "a93fda3", + "released_on": "25-01-2025", + "md5sum": "96d7f2fd3e503e8a80fb4a761d5f6caf" + }, + "3.0.0": { + "api_version": 9, + "commit_sha": "32e5180", + "released_on": "22-01-2025", + "md5sum": "b1742bdaf76b60a2eaf62cdcfebdbd52" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "0c5ce76", + "released_on": "02-07-2023", + "md5sum": "8b05407fda379d853f5c75677b19fd85" + }, + "1.2.1": { + "api_version": 7, + "commit_sha": "64e8a5c", + "released_on": "18-12-2022", + "md5sum": "5237713243bd3ba5dd20a5efc568f40d" + }, + "1.2.0": { + "api_version": 7, + "commit_sha": "b7036af", + "released_on": "27-09-2022", + "md5sum": "c9d694c1beafba7957da34014a38d278" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "3a8ba07", + "released_on": "06-09-2022", + "md5sum": "eb93f3df040261f73963621cb66565d3" + } + } + }, + "advanced_party_window": { + "description": "Advanced your party window with lots of feature", + "external_url": "https://www.youtube.com/watch?v=QrES1jQGXF0", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "2.1.5": { + "api_version": 9, + "commit_sha": "a36e3c2", + "released_on": "28-10-2025", + "md5sum": "3709d73385137f6ae47ee1481213a014" + }, + "2.1.4": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "045bb55d3e87c68ac8770f32b5653b48" + }, + "2.1.3": { + "api_version": 9, + "commit_sha": "0defd84", + "released_on": "05-04-2025", + "md5sum": "9d58abc6e5b9e6779e59a2e2f35f8a3d" + }, + "2.1.2": { + "api_version": 9, + "commit_sha": "c912759", + "released_on": "25-03-2025", + "md5sum": "1d732670a4e8b71fc0e5d9ea9b4a31b4" + }, + "2.1.1": { + "api_version": 9, + "commit_sha": "a93fda3", + "released_on": "25-01-2025", + "md5sum": "7db9b514d97df4c672d5e8ef72b6be6c" + }, + "2.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "2929a3af42ee3547864878a77a4c66a1" + }, + "2.0.2": { + "api_version": 8, + "commit_sha": "32e0f45", + "released_on": "12-11-2023", + "md5sum": "a3c20dd470939ea7831df3e4e007f303" + }, + "2.0.1": { + "api_version": 8, + "commit_sha": "d511c15", + "released_on": "03-08-2023", + "md5sum": "6a9f37d78e2293aa0bf8eb6e5d60e702" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "da0e63d", + "released_on": "02-07-2023", + "md5sum": "7d90768d603fcf91b38ba7fa1d30501e" + }, + "1.0.1": { + "api_version": 7, + "commit_sha": "64e8a5c", + "released_on": "18-12-2022", + "md5sum": "7807b532802d17b77a0017c46ac1cbfb" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "e994af5", + "released_on": "17-09-2022", + "md5sum": "efc4f07eb242d28fe08a7a707376bfc5" + } + } + }, + "auto_stunt": { + "description": "auto stunts, learn stunts, mirror your player, bro elimination mini game", + "external_url": "https://www.youtube.com/watch?v=DXZeWrTCZlI", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "a7301c37600372524f776c6f22c05711" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "8982f0e", + "released_on": "22-01-2025", + "md5sum": "df20190b079bbf80f1e061339930e5f7" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "e80ed956d637392c948725899da135e3" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "6acdea8", + "released_on": "26-11-2022", + "md5sum": "3ced6731c0cbe79c972e7d1c7478ce16" + } + } + }, + "file_share": { + "description": "share/import mods, replays, custom characters/maps with link or QR code", + "external_url": "https://youtu.be/qtGsFU4cgic", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "3ff1933711e7f8a700ca67c2125ca791" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "cb06090af5beb9f80965342b9a83b1e9" + }, + "1.0.2": { + "api_version": 8, + "commit_sha": "505c948", + "released_on": "09-06-2024", + "md5sum": "ae32962255c357b29bd9c46c0551a19c" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "b089293", + "released_on": "08-12-2023", + "md5sum": "d89537f6737829f3527b8ed32a1b12bf" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "dbd41c4", + "released_on": "31-07-2023", + "md5sum": "e1d4852a3c2c2dbf746867016ae9fbdf" + } + } + }, + "server_switch": { + "description": "Let you switch between recents servers", + "external_url": "https://www.youtube.com/watch?v=QrES1jQGXF0", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "2.0.0": { + "api_version": 8, + "commit_sha": "a18a595", + "released_on": "08-07-2023", + "md5sum": "30a21cb1c739b098dcaa791e4a2cd481" + }, + "1.0.1": { + "api_version": 7, + "commit_sha": "64e8a5c", + "released_on": "18-12-2022", + "md5sum": "8efcf38604e5519d66a858cc38868641" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "e994af5", + "released_on": "17-09-2022", + "md5sum": "5b90431822c8f6a8227266ab8d64686b" + } + } + }, + "character_chooser": { + "description": "Let you choose your character before joining game.", + "external_url": "https://www.youtube.com/watch?v=hNmv2l-NahE", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "2.2.2": { + "api_version": 9, + "commit_sha": "7ad11b9", + "released_on": "04-01-2026", + "md5sum": "c7fb5f1c2be9ef3b1fd35177c434dc34" + }, + "2.2.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "4cfb39f7b074c49d9fa403763d194f05" + }, + "2.2.0": { + "api_version": 9, + "commit_sha": "6fc165a", + "released_on": "26-05-2025", + "md5sum": "49817d6f1b42a3a2a2fabae9d70c076f" + }, + "2.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "56d3b51b5929a53a7d2de04326ea7bae" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "0c5ce76", + "released_on": "02-07-2023", + "md5sum": "bb5d85fb528020e809eaebb17a388e32" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "ff4de19", + "released_on": "05-10-2022", + "md5sum": "38d47297d4048a2fe1022ea841c76f91" + } + } + }, + "character_maker": { + "description": "Make new characters by manipulating models and textures.", + "external_url": "https://www.youtube.com/watch?v=q0KxY1hfMPQ", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "2.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "aee18485e1731b792c0ba8844f7b97a3" + }, + "2.1.0": { + "api_version": 9, + "commit_sha": "94742b9", + "released_on": "21-01-2025", + "md5sum": "6fa2e76b9d90ab9a7b801c2245f1a017" + }, + "2.0.1": { + "api_version": 8, + "commit_sha": "a3a48c2", + "released_on": "08-12-2023", + "md5sum": "0adc53345b1616a3356a9032cdfeb82f" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "f7ae806", + "released_on": "03-08-2023", + "md5sum": "57e79be044430139f6218a61e3a107a4" + }, + "1.0.1": { + "api_version": 7, + "commit_sha": "da10318", + "released_on": "10-10-2022", + "md5sum": "b19ff011b6e229544324d4310770d7b9" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "ff4de19", + "released_on": "05-10-2022", + "md5sum": "f734fa33994c7cfafc637b7e3dca60ce" + } + } + }, + "icons_keyboard": { + "description": "Enable 'Always Use Internal Keyboard' in Settings>Advanced. Double tap space-bar to change keyboards", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "" + } + ], + "versions": { + "3.1.1": { + "api_version": 9, + "commit_sha": "6bf081a", + "released_on": "08-02-2025", + "md5sum": "53776b882b3c06be4d28a3d547984c46" + }, + "3.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "d94cc8a8a6cef495a6e09d019ebe3eb9" + }, + "3.0.1": { + "api_version": 8, + "commit_sha": "78f3c44", + "released_on": "27-12-2023", + "md5sum": "b7756605a4bc382329c078c4e399b96b" + }, + "3.0.0": { + "api_version": 8, + "commit_sha": "02da437", + "released_on": "16-12-2023", + "md5sum": "ae221a7c2b938eccf63db493c247991e" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "48f9302", + "released_on": "28-07-2023", + "md5sum": "8a63ad58d24e2b61bb63645f74d876e8" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "b581d90", + "released_on": "02-09-2022", + "md5sum": "94f67a98a9faed0ece63674c84d40061" + } + } + }, + "pro_unlocker": { + "description": "Unlocks some pro-only features - custom colors, playlist maker, etc. (Please support the game developer if you can!)", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "2.0.0": { + "api_version": 8, + "commit_sha": "a18a595", + "released_on": "08-07-2023", + "md5sum": "7403fcea1855ddf561aa412d703fcc6e" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "41e239c", + "released_on": "18-09-2022", + "md5sum": "bbaee5f133b41d2eb53e3b726403a75e" + } + } + }, + "ultra_party_window": { + "description": "Ultra your party window with lots of features", + "external_url": "", + "authors": [ + { + "name": "Droopy", + "email": "", + "discord": "droopy25" + } + ], + "versions": { + "4.0.0": { + "api_version": 7, + "commit_sha": "a23e8cd", + "released_on": "04-10-2022", + "md5sum": "7da7fae6ddf2560789bbef56f4ff5bd6" + } + } + }, + "quickturn": { + "description": "Sharp turns while running (releasing run button, changing direction, holding run again) are much faster with this mod, allowing for more dynamic, aggressive play.", + "external_url": "", + "authors": [ + { + "name": "TheMikirog", + "email": "", + "discord": "themikirog" + } + ], + "versions": { + "3.0.3": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "5fcf81134df8df721ba5178d43135f11" + }, + "3.0.2": { + "api_version": 9, + "commit_sha": "43b3a41", + "released_on": "21-01-2025", + "md5sum": "3f6ad4656e2a04fc1a767f2760407c7b" + }, + "3.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "697f1204f7722f27f2bdbbff3994763c" + }, + "3.0.0": { + "api_version": 7, + "commit_sha": "0841b9e", + "released_on": "14-11-2022", + "md5sum": "417cd87c17034b4eac8bc301d294d708" + } + } + }, + "ragdoll_b_gone": { + "description": "Removes ragdolls. Thanos snaps those pesky feet-tripping body sacks out of existence.", + "external_url": "", + "authors": [ + { + "name": "TheMikirog", + "email": "", + "discord": "themikirog" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "a983692f04aa623d109d3946ac801ebd" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "90aaf343cf22eb024665ec5a7a4889c0" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "7b1dd1432930e6dc198780a134b88c88" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "0841b9e", + "released_on": "14-11-2022", + "md5sum": "dccb150ef67440f1db7e39884bd856b3" + } + } + }, + "bomb_radius_visualizer": { + "description": "With this cutting edge technology, you precisely know how close to the bomb you can tread. Supports modified blast radius values!", + "external_url": "", + "authors": [ + { + "name": "TheMikirog", + "email": "", + "discord": "themikirog" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "4c1bb08617778f70e4ec8d506b477760" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "4281fd65d90f314d3c8180505856a9d3" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "7313a54c35611e9d8f7d0854b6646bc7" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "0841b9e", + "released_on": "14-11-2022", + "md5sum": "6213bc2573cb83f8bf72030604801f5a" + } + } + }, + "disco_light": { + "description": "Add disco light into the game. Do '/disco' to turn on and '/disco off' to turn off", + "external_url": "", + "authors": [ + { + "name": "Cross Joy", + "email": "cross.joy.official@gmail.com", + "discord": "crossjoy" + } + ], + "versions": { + "2.2.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "eac16eed6564334b027d451458afb8c6" + }, + "2.2.0": { + "api_version": 9, + "commit_sha": "04b8460", + "released_on": "21-01-2025", + "md5sum": "2c78b1e3918053fa94d709571fef624d" + }, + "2.1.0": { + "api_version": 8, + "commit_sha": "41046ae", + "released_on": "14-02-2024", + "md5sum": "bfffb346d61958c02015dde0b1520ca3" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "bb75b79a749f26ed359e0fc99f23a958" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "800125c", + "released_on": "14-12-2022", + "md5sum": "616f31da667ea3663efe449c67a0e032" + } + } + }, + "practice_tools": { + "description": "Powerful and comprehensive tools for practice purposes. Practice tabs can be accessed through party window.", + "external_url": "https://www.youtube.com/watch?v=7BeCcIYSXd0&t=113s&ab_channel=BOMBsquadlife", + "authors": [ + { + "name": "Cross Joy", + "email": "cross.joy.official@gmail.com", + "discord": "crossjoy" + } + ], + "versions": { + "3.0.0": { + "api_version": 9, + "commit_sha": "a696ade", + "released_on": "19-09-2025", + "md5sum": "2f4e245fbae235f6f7f3fbfb3403ec40" + }, + "2.2.0": { + "api_version": 8, + "commit_sha": "41046ae", + "released_on": "14-02-2024", + "md5sum": "b02de60a80039b092ef4d826b3d61cc1" + }, + "2.1.1": { + "api_version": 8, + "commit_sha": "5422dd6", + "released_on": "18-01-2024", + "md5sum": "f2b5e4ff71c3952f57957ca3ab0c2e00" + }, + "2.1.0": { + "api_version": 8, + "commit_sha": "dcfe582", + "released_on": "09-12-2023", + "md5sum": "f2b5e4ff71c3952f57957ca3ab0c2e00" + }, + "2.0.1": { + "api_version": 8, + "commit_sha": "41cc38a", + "released_on": "01-10-2023", + "md5sum": "9ed00b2c86bd62168aa1cab6ea5fdfe4" + }, + "2.0.0": { + "api_version": 8, + "commit_sha": "9340deb", + "released_on": "31-07-2023", + "md5sum": "5cc5d2d1d775c726b6065679dcfe3bda" + }, + "1.2.0": { + "api_version": 7, + "commit_sha": "e61958b", + "released_on": "19-06-2023", + "md5sum": "0ef91a10240b1df25595750015042a3f" + }, + "1.1.0": { + "api_version": 7, + "commit_sha": "7ab4576", + "released_on": "15-06-2023", + "md5sum": "f439f1a9c325f6ac09f2bab23b10e275" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "800125c", + "released_on": "14-12-2022", + "md5sum": "a04c30c11a43443fe192fe70ad528f22" + } + } + }, + "health_indicator": { + "description": "Add a simple health indicator on every player and bot.", + "external_url": "", "authors": [ { - "name": "Rikko", - "email": "rikkolovescats@proton.me", - "discord": "Rikko#7383" + "name": "Cross Joy", + "email": "cross.joy.official@gmail.com", + "discord": "crossjoy" + } + ], + "versions": { + "1.3.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "0f1a6208603a463476ae07c9f31c22d5" + }, + "1.3.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "e9b9817c356101038e4f3b522311c4a6" }, + "1.2.0": { + "api_version": 8, + "commit_sha": "41046ae", + "released_on": "14-02-2024", + "md5sum": "e16123cf550f433c70e53b8ec41a4e46" + } + } + }, + "autorun": { + "description": "Run without holding any buttons. Made for beginners or players on mobile. Keeps your character maneuverable. Start running as usual to override.", + "external_url": "", + "authors": [ { - "name": "Vishal", - "email": "vishal.u338@gmail.com", - "discord": "𝑽𝑰𝑺𝑯𝑼𝑼𝑼#2921" + "name": "TheMikirog", + "email": "", + "discord": "themikirog" } ], "versions": { - "1.2.0": { - "api_version": 7, - "commit_sha": "a98dacd", - "dependencies": [], - "released_on": "12-08-2022", - "md5sum": "41084bfec41119ca9df8e6d899cd3cc0" + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "b500fe0d358ab1aaba1e4fc4f1475b8d" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "f269311e8c321f912940c8127e4c6c29" + }, + "1.0.2": { + "api_version": 8, + "commit_sha": "95076d1", + "released_on": "23-06-2024", + "md5sum": "3a072f0bb33bfa784670827d1b32b4d9" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "c472b9ba7be0a1f109a757c1c06b25cd" }, - "1.1.0": { - "api_version": 7, - "commit_sha": "13a9d128", - "dependencies": [], - "released_on": "03-06-2022", - "md5sum": "4b6bbb99037ebda4664da7c510b3717c" + "1.0.0": { + "api_version": 7, + "commit_sha": "cb2d952", + "released_on": "01-01-2023", + "md5sum": "22f54996dc55008267d09bf48a2cffe3" } } }, - "store_event_specials": { - "description": "Exposes event-special store items not normally available for purchase", + "tnt_respawn_text": { + "description": "Shows when a TNT box is about to respawn with non-intrusive text.", + "external_url": "", + "authors": [ + { + "name": "TheMikirog", + "email": "", + "discord": "themikirog" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "5ecab5d2bd64d036c8281b0eb1c56b0c" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "b33c016195bc17c3ebd878570c9e3fa6" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "264b14d7ec65453b74d4680d507fcb4f" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "05ffa9f", + "released_on": "14-01-2023", + "md5sum": "cc1738b0326c9679453bdf1489ded483" + } + } + }, + "allow_invisible_models": { + "description": "Changing model to None will make it invisible instead of raising an exception.", "external_url": "", "authors": [ { "name": "Rikko", "email": "rikkolovescats@proton.me", - "discord": "Rikko#7383" + "discord": "rikkolovescats" + } + ], + "versions": { + "1.2.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "96815ba9a784ca0004ce6580eedd9b57" + }, + "1.0.1": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "d4b1c74d4c6e6f893f0b50c4f863720e" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "3221b3a", + "released_on": "22-01-2023", + "md5sum": "24913c665d05c3056c8ba390fe88155e" + } + } + }, + "random_play": { + "description": "Play randomly generated FFA or Teams matches from your installed minigames and maps.", + "external_url": "", + "authors": [ + { + "name": "silver_volt4", + "email": "", + "discord": "Silver_Volt4#6502" + } + ], + "versions": { + "1.1.2": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "f4e51ccb7558b061e9a60b4e20510a8c" + }, + "1.1.1": { + "api_version": 9, + "commit_sha": "3540260", + "released_on": "21-01-2025", + "md5sum": "94270effa9a80a138a9c083aca087fd7" + }, + "1.1.0": { + "api_version": 8, + "commit_sha": "5fb8195", + "released_on": "28-06-2023", + "md5sum": "f6c1105b34d0426327688841d7e89bb9" + }, + "1.0.1": { + "api_version": 7, + "commit_sha": "3ef572f", + "released_on": "25-05-2023", + "md5sum": "f093c09b3e9439755a515ae6751e1f7e" + }, + "1.0.0": { + "api_version": 7, + "commit_sha": "d91fea3", + "released_on": "25-05-2023", + "md5sum": "cec9a9c6f6aebddd8ec8160f7cc94b57" + } + } + }, + "menu_theme": { + "description": "A simple UI mod for customizing the Main Menu's appearance. Go to your profiles for use", + "external_url": "", + "authors": [ + { + "name": "Yann", + "email": "", + "discord": "riyukiiyan" + } + ], + "versions": { + "1.0.1": { + "api_version": 8, + "commit_sha": "53ce44a", + "released_on": "29-12-2023", + "md5sum": "41e324d84bc79bf1283b643014802069" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "f829aca", + "released_on": "26-08-2023", + "md5sum": "4cca8e04efadf39b2be51b5b95c4e5a4" + } + } + }, + "discord_richpresence": { + "description": "Discord Rich Presence for Bombsquad.", + "external_url": "https://youtu.be/SbbG9V74_E4", + "authors": [ + { + "name": "Dliwk&brostos", + "email": "", + "discord": "dliwk&brostos" + } + ], + "versions": { + "1.6.0": { + "api_version": 9, + "commit_sha": "b7b57a0", + "released_on": "26-05-2025", + "md5sum": "ce3b57ad8cbcc5834b589030b72830b4" + }, + "1.5.2": { + "api_version": 9, + "commit_sha": "0db98e8", + "released_on": "26-01-2025", + "md5sum": "b00b046f8b2334d25644315c05bbc5bc" + }, + "1.5.1": { + "api_version": 9, + "commit_sha": "a93fda3", + "released_on": "25-01-2025", + "md5sum": "d33cd8c65833278f35bd87013a80e883" + }, + "1.5.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "af4ec55092fb22d2799d63e19c599464" + }, + "1.4.3": { + "api_version": 8, + "commit_sha": "f50a546", + "released_on": "12-08-2024", + "md5sum": "44e294a7b04a3e03b96ad430afb40d20" + }, + "1.4.2": { + "api_version": 8, + "commit_sha": "2b5c9ee", + "released_on": "22-01-2024", + "md5sum": "7301fb55984f9caa4018bab40c4945d0" + }, + "1.4.1": { + "api_version": 8, + "commit_sha": "48c8abb", + "released_on": "08-12-2023", + "md5sum": "087728c68fb9505b06beccc9ceb8acbf" + }, + "1.4.0": { + "api_version": 8, + "commit_sha": "6947777", + "released_on": "03-08-2023", + "md5sum": "1b8a85ed902e0cc7e6cebad63c984544" + }, + "1.3.0": { + "api_version": 8, + "commit_sha": "7c8953c", + "released_on": "01-08-2023", + "md5sum": "1db3733c3d7d26165d75091cd8887db6" + }, + "1.2.0": { + "api_version": 8, + "commit_sha": "150f49f", + "released_on": "28-07-2023", + "md5sum": "3b13d83518865a2b07b9340d59d0ab92" + }, + "1.1.0": { + "api_version": 8, + "commit_sha": "90fff9b", + "released_on": "23-07-2023", + "md5sum": "69723f76a0114fe99d6c85715ad4eb49" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "230d12d", + "released_on": "18-07-2023", + "md5sum": "5fa8706f36d618f8302551dd2a0403a0" + } + } + }, + "disable_friendly_fire": { + "description": "Disables friendly fire", + "external_url": "", + "authors": [ + { + "name": "EmperoR", + "email": "", + "discord": "EmperoR#4098" + } + ], + "versions": { + "1.0.2": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "d62deba37a1479ae3809abfa5bebcf7f" + }, + "1.0.1": { + "api_version": 9, + "commit_sha": "ea64940", + "released_on": "21-01-2025", + "md5sum": "9742d478095ac72b1780b75c17ebeb07" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "f744c41", + "released_on": "24-01-2024", + "md5sum": "203ecefa1c1eb9894cfb0d87e2d7fe09" + } + } + }, + "infinity_shield": { + "description": "Gives you unbreakable shield", + "external_url": "https://youtu.be/hp7vbB-hUPg?si=i7Th0NP5xDPLN2P_", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "47ce5018a75b6d215dbb00264a8c4a4d" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "85193fe", + "released_on": "21-01-2025", + "md5sum": "03d98165e14886fba7f139f8610686f7" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "718039b", + "released_on": "24-01-2024", + "md5sum": "eb917ca19d206dfd19667181dacc1df5" + } + } + }, + "only_night": { + "description": "Night Mode", + "external_url": "", + "authors": [ + { + "name": "-", + "email": "", + "discord": "" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "9cc64ea5a18f058a7656e2d23cb22d26" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "4cd035b88234f5f082c706d5eb37bd28" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "718039b", + "released_on": "24-01-2024", + "md5sum": "255d3d6694008cc2f73d115182100b52" + } + } + }, + "tag": { + "description": "Get a tag", + "external_url": "", + "authors": [ + { + "name": "pranav", + "email": "", + "discord": "" + } + ], + "versions": { + "2.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "89a39d7ff9987d047fe543438cf02e14" + }, + "2.1.0": { + "api_version": 9, + "commit_sha": "7d6ed3d", + "released_on": "22-01-2025", + "md5sum": "c3af893247488915d26585d0957171d8" + }, + "2.0.1": { + "api_version": 8, + "commit_sha": "718039b", + "released_on": "24-01-2024", + "md5sum": "01cf9e10ab0e1bf51c07d80ff842c632" + } + } + }, + "xyz_tool": { + "description": "Punch to save the co-ordinates", + "external_url": "", + "authors": [ + { + "name": "Yann", + "email": "", + "discord": "riyukiiyan" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "b86314cf7d62fbcc95a295ebcbc42839" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "e2c6477", + "released_on": "21-01-2025", + "md5sum": "480b825e187e59020b339290966b5c5a" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "5063690", + "released_on": "03-02-2024", + "md5sum": "3f301456128f422b7277c667f6c5c47e" + } + } + }, + "bots_can_accept_powerups": { + "description": "Bots can steal your powerups", + "external_url": "https://youtu.be/Jrk5JfveYEI?si=wYXWVxdC-3XMpuCg", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "ff0ed2dfada025c9d7269eaac1091062" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "bb129cc", + "released_on": "14-01-2025", + "md5sum": "50e9c3ea8d7e7de5cd491987547c9cf3" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "4941d0c", + "released_on": "01-02-2024", + "md5sum": "71eae7c5d0e05821809d348ea0c64837" + } + } + }, + "cheat_menu": { + "description": "Cheat menu on the settings window", + "external_url": "", + "authors": [ + { + "name": "pranav", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 8, + "commit_sha": "4941d0c", + "released_on": "01-02-2024", + "md5sum": "079e857197979aabf53f232b3cce56ba" + } + } + }, + "natpmp_upnp": { + "description": "Automatic port forwarding if upnp is enabled", + "external_url": "", + "authors": [ + { + "name": "brostosjoined", + "email": "", + "discord": "brostos" + } + ], + "versions": { + "1.5.0": { + "api_version": 9, + "commit_sha": "529fd6a", + "released_on": "26-01-2025", + "md5sum": "3ced77595e949d19e718e0e3ae42af56" + }, + "1.0.0": { + "api_version": 8, + "commit_sha": "48bd0da", + "released_on": "03-03-2024", + "md5sum": "15f969c23d19118d4898570cfae71c7b" + } + } + }, + "wave_emote": { + "description": "Type `hello` in chat while in game and get a wave emote", + "external_url": "", + "authors": [ + { + "name": "brostosjoined", + "email": "", + "discord": "brostos" + } + ], + "versions": { + "1.5.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "73d59a56a384a16c039a5a1ec333ec6c" + }, + "1.5.0": { + "api_version": 9, + "commit_sha": "0db98e8", + "released_on": "26-01-2025", + "md5sum": "472846d138af954b8b8085975d016a7c" + } + } + }, + "fast_epic_toggle": { + "description": "Switch between Epic/Fast mode easier in-game!", + "external_url": "", + "authors": [ + { + "name": "imAnesYT", + "email": "", + "discord": "v4_y" + } + ], + "versions": { + "1.1.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "696744aa6f40b5a3f24d2df716a6a1bc" + }, + "1.1.0": { + "api_version": 9, + "commit_sha": "6e438dc", + "released_on": "09-03-2025", + "md5sum": "dd3788ea357082a0eca9345de492206c" + }, + "1.0.0": { + "api_version": 9, + "commit_sha": "541828a", + "released_on": "02-03-2025", + "md5sum": "756c221489a0759ff8f509cc22a2a823" + } + } + }, + "vanilla_wiggle_dance": { + "description": "Wiggle left and right to do a silly dance!", + "external_url": "https://youtu.be/g7Neiz3dB3U", + "authors": [ + { + "name": "SoK", + "email": "", + "discord": "sok05" + } + ], + "versions": { + "1.0.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "90887e8a72aa799d93a21ffb0d3b7e5c" + }, + "1.0.0": { + "api_version": 9, + "commit_sha": "e6b0658", + "released_on": "09-04-2025", + "md5sum": "c4a5ac3f3b5aa3de5f4b659e40106a95" + } + } + }, + "quick_chat": { + "description": "Easier send messages, add/remove messages", + "external_url": "", + "authors": [ + { + "name": "imAnesYT", + "email": "", + "discord": "v4_y" + } + ], + "versions": { + "1.0.2": { + "api_version": 9, + "commit_sha": "44b03b8", + "released_on": "16-07-2025", + "md5sum": "6934293c2444ceb61f4957f80d57503a" + }, + "1.0.1": { + "api_version": 9, + "commit_sha": "e604a3c", + "released_on": "23-06-2025", + "md5sum": "f9b5ccc755fdbd0cef4ce3b566c6231e" + }, + "1.0.0": { + "api_version": 9, + "commit_sha": "c4ea42b", + "released_on": "25-05-2025", + "md5sum": "ce2767d38676fda5be07d1608b80b5bb" + } + } + }, + "update_notifier": { + "description": "Notifies when a new update is available", + "external_url": "", + "authors": [ + { + "name": "brostosjoined", + "email": "", + "discord": "brostos" + } + ], + "versions": { + "1.0.0": { + "api_version": 9, + "commit_sha": "4aca367", + "released_on": "27-05-2025", + "md5sum": "3ad8036b3491588bdc8a002803f2df58" + } + } + }, + "arabic_keyboard": { + "description": "Add arabic language to interal keyboard", + "external_url": "", + "authors": [ + { + "name": "imAnesYT", + "email": "", + "discord": "v4_y" + } + ], + "versions": { + "1.0.0": { + "api_version": 9, + "commit_sha": "665d7d4", + "released_on": "28-05-2025", + "md5sum": "7d1f771d40103c4cd3f9719353516ba7" + } + } + }, + "account_switcher": { + "description": "Switch between multiple accounts", + "external_url": "", + "authors": [ + { + "name": "LoupGarou", + "email": "LoupGarou5418@outlook.com", + "discord": "loupgarou_" + } + ], + "versions": { + "1.1.0": { + "api_version": 9, + "commit_sha": "a694c98", + "released_on": "30-12-2025", + "md5sum": "65ba0c35c08ace82141945c56f87142a" + }, + "1.0.0": { + "api_version": 9, + "commit_sha": "f62ed2a", + "released_on": "06-09-2025", + "md5sum": "01c6cb1a3d3b525c87caed389c8d03ed" + } + } + }, + "floating_star": { + "description": "Get floating stars with colorful text", + "external_url": "", + "authors": [ + { + "name": "BsRush_Mod", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": { + "api_version": 9, + "commit_sha": "be999fc", + "released_on": "18-01-2026", + "md5sum": "c0fc8f5d36b24977e1c997e4211601a7" + } + } + }, + "powerup_manager": { + "description": "This plugin add new modded powerups and features to manage them", + "external_url": "", + "authors": [ + { + "name": "ATD", + "email": "anasdhaoidi001@gmail.com", + "discord": "" } ], "versions": { - "1.0.0": { - "api_version": 7, - "commit_sha": "2aa6df31", - "dependencies": [], - "released_on": "06-08-2022", - "md5sum": "233dfaa7f0e9394d21454f4ffa7d0205" + "1.0.0": { + "api_version": 9, + "commit_sha": "f38a7ac", + "released_on": "21-01-2026", + "md5sum": "120276a8d215248888e56bfc86cc66f5" } } } } -} +} \ No newline at end of file diff --git a/plugins/utilities/account_switcher.py b/plugins/utilities/account_switcher.py new file mode 100644 index 000000000..10ed09a75 --- /dev/null +++ b/plugins/utilities/account_switcher.py @@ -0,0 +1,312 @@ +""" Plugin by LoupGarou a.k.a Loup/Soup + Discord → loupgarou_ +Switch between multiple accounts in easily + +Feel free to let me know if you use this plugin,i love to hear that :) + +Message me in discord if you find some bug +Use this code for your experiments or plugin but please dont rename this plugin and distribute with your name +""" + +# ba_meta require api 9 + +from __future__ import annotations +import babase +import bauiv1 as bui +from bauiv1lib.confirm import ConfirmWindow +from bauiv1lib.account.settings import AccountSettingsWindow +from os import listdir, path, mkdir, remove +from shutil import copy, rmtree +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Optional + +UI_SCALE = 2.0 if (babase.app.ui_v1.uiscale == babase.UIScale.SMALL) else 1.0 + +ACCOUNT_FILES = ['.bsac2', '.bsuuid', 'config.json', '.config_prev.json'] +plus = babase.app.plus +env = babase.app.env +USER_DIR = path.dirname(env.config_file_path) +ACCOUNTS_DIR = path.join(USER_DIR, 'account_switcher_profiles') + +if not path.exists(ACCOUNTS_DIR): + mkdir(ACCOUNTS_DIR) + + +def print_msg(text: str, color=(0.3, 1, 0.3)): + bui.screenmessage(text, color=color) + + +class AccountSwitcherUI(bui.Window): + def __init__(self): + # Base dimensions; the final size is controlled by the scale property. + self._width = 600 + self._height = 400 + + self._root_widget = bui.containerwidget( + size=(self._width, self._height), + scale=UI_SCALE, # Apply the global scale here + transition='in_right', + stack_offset=(0, 0) + ) + + # Standard back/close button + self._back_button = bui.buttonwidget( + parent=self._root_widget, + position=(40, self._height - 60), + size=(40, 40), + scale=1.0, + label=babase.charstr(babase.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self._close, + ) + bui.containerwidget(edit=self._root_widget, cancel_button=self._back_button) + + # Title + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 40), + size=(0, 0), + h_align='center', + v_align='center', + text='Account Switcher', + scale=1.5, + color=(0.8, 0.8, 0.9), + ) + + content_height = self._height - 80 + content_y = 20 + + # Buttons stacked on the right + btn_col_width = 180 + btn_col_x = self._width - btn_col_width - 20 + btn_width = 160 + btn_height = 60 + btn_x = btn_col_x + (btn_col_width - btn_width) * 0.5 + v_pos = content_y + content_height - btn_height + v_step = btn_height + 10 + + bui.buttonwidget( + parent=self._root_widget, + position=(btn_x, v_pos), + size=(btn_width, btn_height), + label='Save Current', + on_activate_call=self.save_current_account, + ) + v_pos -= v_step + + bui.buttonwidget( + parent=self._root_widget, + position=(btn_x, v_pos), + size=(btn_width, btn_height), + label='Add New Account', + on_activate_call=self.add_new_account, + ) + v_pos -= v_step + + bui.buttonwidget( + parent=self._root_widget, + position=(btn_x, v_pos), + size=(btn_width, btn_height), + label='Load Selected', + on_activate_call=self.load_selected_account, + ) + v_pos -= v_step + + bui.buttonwidget( + parent=self._root_widget, + position=(btn_x, v_pos), + size=(btn_width, btn_height), + label='Delete Selected', + on_activate_call=self.delete_selected_account, + ) + + # List box on the left + self.list_width = btn_col_x - 30 + list_x = 20 + + scroll = bui.scrollwidget( + parent=self._root_widget, + position=(list_x, content_y), + size=(self.list_width, content_height), + ) + bui.containerwidget(edit=scroll, claims_left_right=True) + + self._list = bui.columnwidget( + parent=scroll, + background=False, + border=0, + ) + + self._selected_profile: Optional[str] = None + self._profile_widgets: list[bui.Widget] = [] + + self._refresh_account_list() + + def _close(self) -> None: + bui.containerwidget(edit=self._root_widget, transition='out_right') + + def _refresh_account_list(self): + for widget in self._profile_widgets: + widget.delete() + self._profile_widgets = [] + + profiles = sorted(listdir(ACCOUNTS_DIR)) + for prof in profiles: + text_widget = bui.textwidget( + parent=self._list, + text=prof, + size=(self.list_width, 30), + color=(1, 1, 1), + selectable=True, + click_activate=True, + max_chars=40, + corner_scale=1.2, + ) + self._profile_widgets.append(text_widget) + bui.textwidget( + edit=text_widget, on_activate_call=babase.Call( + self.on_select_profile, prof, text_widget) + ) + + def on_select_profile(self, profile_name: str, selected_widget: bui.Widget): + self._selected_profile = profile_name + for widget in self._profile_widgets: + bui.textwidget(edit=widget, color=(1, 1, 1)) + bui.textwidget(edit=selected_widget, color=(1, 1, 0.2)) + + def get_current_account(self) -> Optional[str]: + if plus.get_v1_account_state() == 'signed_in': + return plus.get_v1_account_display_string() + return None + + def save_current_account(self): + name = self.get_current_account() + if not name: + print_msg("No account signed in!", color=(1, 0, 0)) + return + + account_folder = path.join(ACCOUNTS_DIR, name) + if not path.exists(account_folder): + mkdir(account_folder) + + for fname in ACCOUNT_FILES: + src = path.join(USER_DIR, fname) + if path.exists(src): + try: + copy(src, path.join(account_folder, fname)) + except IOError as e: + print_msg(f"Error saving {fname}: {e}", color=(1, 0, 0)) + + print_msg(f"Saved current account as '{name}'") + self._refresh_account_list() + + def add_new_account(self) -> None: + def do_action(): + self.save_current_account() + for fname in ACCOUNT_FILES: + file_path = path.join(USER_DIR, fname) + if path.exists(file_path): + remove(file_path) + print_msg('Account files removed.') + + ConfirmWindow( + text='This will save your current login and then shutdown the game.\nAre you sure?', + action=lambda: self.lock_call_exit(do_action), + ok_text='Confirm & Logout', + cancel_is_selected=True, + ) + + def lock_call_exit(self, callable_action): + babase.suppress_config_and_state_writes() + callable_action() + babase.apptimer(1.5, babase.quit) + + def load_selected_account(self): + if not self._selected_profile: + print_msg("No account selected to load!", color=(1, 0, 0)) + return + account_folder = path.join(ACCOUNTS_DIR, self._selected_profile) + + self.save_current_account() + + def do_switch(): + for fname in ACCOUNT_FILES: + dest = path.join(USER_DIR, fname) + src = path.join(account_folder, fname) + if path.exists(dest): + remove(dest) + if path.exists(src): + copy(src, dest) + print_msg(f"Loaded account {self._selected_profile}") + + ConfirmWindow( + text=f"Load account {self._selected_profile}?\nGame will shut down.", + action=lambda: self.lock_call_exit(do_switch), + cancel_is_selected=True, + ) + + def delete_selected_account(self): + if not self._selected_profile: + print_msg("No account selected to delete!", color=(1, 0, 0)) + return + account_folder = path.join(ACCOUNTS_DIR, self._selected_profile) + + def do_delete(): + if path.exists(account_folder): + rmtree(account_folder) + print_msg(f"Deleted account '{self._selected_profile}'", color=(1, 0.5, 0.5)) + self._selected_profile = None + self._refresh_account_list() + + ConfirmWindow( + text=f"Delete account '{self._selected_profile}' permanently?", + action=do_delete, + cancel_is_selected=True, + ) + + +# --- Monkey-Patching --- +_original_account_settings_init = AccountSettingsWindow.__init__ +_original_on_adapter_sign_in_result = AccountSettingsWindow._on_adapter_sign_in_result + + +def new_account_settings_init(self, *args, **kwargs): + _original_account_settings_init(self, *args, **kwargs) + button_width = 350 + # Use a lambda to create an instance of the class when the button is pressed. + bui.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, -25), + size=(button_width, 60), + label='Switch Accounts...', + on_activate_call=lambda: AccountSwitcherUI() + ) + + +def new_on_adapter_sign_in_result(self, result: str) -> None: + # First, call the original method to ensure default behavior runs. + _original_on_adapter_sign_in_result(self, result) + print(result) + # Now, "capture" the result with our custom logic. + if result == 'success': + print_msg('Sign-in Successful!', color=(0, 1, 0)) + # You could add other logic here, like automatically saving the new account. + elif result != 'cancel': # Don't show a message on user cancellation. + print_msg(f'Sign-in failed: {result}', color=(1, 0, 0)) + +# ba_meta export babase.Plugin + + +class EntryPoint(babase.Plugin): + def on_app_running(self): + # Apply both monkey-patches when the app runs. + AccountSettingsWindow.__init__ = new_account_settings_init + AccountSettingsWindow._on_adapter_sign_in_result = new_on_adapter_sign_in_result + + def has_settings_ui(self): + return True + + def show_settings_ui(self, button=None): + AccountSwitcherUI() diff --git a/plugins/utilities/advanced_party_window.py b/plugins/utilities/advanced_party_window.py new file mode 100644 index 000000000..6bdfd2e4b --- /dev/null +++ b/plugins/utilities/advanced_party_window.py @@ -0,0 +1,2162 @@ +# -*- coding: utf-8 -*- +# ba_meta require api 9 +''' +AdvancedPartyWindow by Mr.Smoothy + +Updated to API 8 on 2nd July 2023 by Mr.Smoothy + +build on base of plasma's modifypartywindow + +discord mr.smoothy#5824 + +https://discord.gg/ucyaesh + + +Youtube : Hey Smoothy + +Download more mods from +https://bombsquad-community.web.app/mods + +''' + +# added advanced ID revealer +# live ping + +# Made by Mr.Smoothy - Plasma Boson +import traceback +import codecs +import json +import re +import sys +import shutil +import copy +import urllib +import os +from bauiv1lib.account import viewer +from bauiv1lib.popup import PopupMenuWindow, PopupWindow +from babase._general import Call +import base64 +import datetime +import ssl +import bauiv1lib.party as bascenev1lib_party +from typing import List, Sequence, Optional, Dict, Any, Union +from bauiv1lib.colorpicker import ColorPickerExact +from bauiv1lib.confirm import ConfirmWindow +from dataclasses import dataclass +import math +import time +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +from typing import TYPE_CHECKING, cast +import urllib.request +import urllib.parse +from _thread import start_new_thread +import threading +version_str = "7" +BCSSERVER = 'mods.ballistica.workers.dev' + +cache_chat = [] +connect = bs.connect_to_party +disconnect = bs.disconnect_from_host +unmuted_names = [] +smo_mode = 3 +f_chat = False +chatlogger = False +screenmsg = True +ip_add = "127.0.0.1" +p_port = 43210 +p_name = "local" +current_ping = 0.0 +enable_typing = False # this will prevent auto ping to update textwidget when user actually typing chat message +ssl._create_default_https_context = ssl._create_unverified_context + + +def newconnect_to_party(address, port=43210, print_progress=False): + global ip_add + global p_port + bs.chatmessage(" Joined IP "+ip_add+" PORT "+str(p_port)) + dd = bs.get_connection_to_host_info_2() + title = getattr(dd, 'name', '') + bs.chatmessage(title) if dd and title else None + if (bool(dd)): + bs.disconnect_from_host() + + ip_add = address + p_port = port + connect(address, port, print_progress) + else: + ip_add = address + p_port = port + # print(ip_add,p_port) + connect(ip_add, port, print_progress) + + +DEBUG_SERVER_COMMUNICATION = False +DEBUG_PROCESSING = False + + +class PingThread(threading.Thread): + """Thread for sending out game pings.""" + + def __init__(self): + super().__init__() + + def run(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + bui.app.classic.ping_thread_count += 1 + sock: Optional[socket.socket] = None + try: + import socket + from babase._net import get_ip_address_type + socket_type = get_ip_address_type(ip_add) + sock = socket.socket(socket_type, socket.SOCK_DGRAM) + sock.connect((ip_add, p_port)) + + accessible = False + starttime = time.time() + + # Send a few pings and wait a second for + # a response. + sock.settimeout(1) + for _i in range(3): + sock.send(b'\x0b') + result: Optional[bytes] + try: + # 11: BA_PACKET_SIMPLE_PING + result = sock.recv(10) + except Exception: + result = None + if result == b'\x0c': + # 12: BA_PACKET_SIMPLE_PONG + accessible = True + break + time.sleep(1) + ping = (time.time() - starttime) * 1000.0 + global current_ping + current_ping = round(ping, 2) + except ConnectionRefusedError: + # Fine, server; sorry we pinged you. Hmph. + pass + except OSError as exc: + import errno + + # Ignore harmless errors. + if exc.errno in { + errno.EHOSTUNREACH, errno.ENETUNREACH, errno.EINVAL, + errno.EPERM, errno.EACCES + }: + pass + elif exc.errno == 10022: + # Windows 'invalid argument' error. + pass + elif exc.errno == 10051: + # Windows 'a socket operation was attempted + # to an unreachable network' error. + pass + elif exc.errno == errno.EADDRNOTAVAIL: + if self._port == 0: + # This has happened. Ignore. + pass + elif babase.do_once(): + print(f'Got EADDRNOTAVAIL on gather ping' + f' for addr {self._address}' + f' port {self._port}.') + else: + babase.print_exception( + f'Error on gather ping ' + f'(errno={exc.errno})', once=True) + except Exception: + babase.print_exception('Error on gather ping', once=True) + finally: + try: + if sock is not None: + sock.close() + except Exception: + babase.print_exception('Error on gather ping cleanup', once=True) + + bui.app.classic.ping_thread_count -= 1 + time.sleep(4) + self.run() + + +RecordFilesDir = os.path.join(_babase.env()["python_directory_user"], "Configs" + os.sep) +if not os.path.exists(RecordFilesDir): + os.makedirs(RecordFilesDir) + +version_str = "3.0.1" + +Current_Lang = None + +SystemEncode = sys.getfilesystemencoding() +if not isinstance(SystemEncode, str): + SystemEncode = "utf-8" + + +PingThread().start() + + +class chatloggThread(): + """Thread for sending out game pings.""" + + def __init__(self): + super().__init__() + self.saved_msg = [] + + def run(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + global chatlogger + self.timerr = babase.AppTimer(5.0, self.chatlogg, repeat=True) + + def chatlogg(self): + global chatlogger + chats = bs.get_chat_messages() + for msg in chats: + if msg in self.saved_msg: + pass + else: + self.save(msg) + self.saved_msg.append(msg) + if len(self.saved_msg) > 45: + self.saved_msg.pop(0) + if chatlogger: + pass + else: + self.timerr = None + + def save(self, msg): + x = str(datetime.datetime.now()) + t = open(os.path.join(_babase.env()["python_directory_user"], "Chat logged.txt"), "a+") + t.write(x+" : " + msg + "\n") + t.close() + + +class mututalServerThread(): + def run(self): + self.timer = babase.AppTimer(10, self.checkPlayers, repeat=True) + + def checkPlayers(self): + if bool(bs.get_connection_to_host_info_2()): + server_name = bs.get_connection_to_host_info_2().name + players = [] + for ros in bs.get_game_roster(): + players.append(ros["display_string"]) + start_new_thread(dump_mutual_servers, (players, server_name,)) + + +def dump_mutual_servers(players, server_name): + filePath = os.path.join(RecordFilesDir, "players.json") + data = {} + if os.path.isfile(filePath): + f = open(filePath, "r") + data = json.load(f) + for player in players: + if player in data: + if server_name not in data[player]: + data[player].insert(0, server_name) + data[player] = data[player][:3] + else: + data[player] = [server_name] + f = open(filePath, "w") + json.dump(data, f) + + +mututalServerThread().run() + + +class customchatThread(): + """.""" + + def __init__(self): + super().__init__() + global cache_chat + self.saved_msg = [] + try: + chats = bs.get_chat_messages() + for msg in chats: # fill up old chat , to avoid old msg popup + cache_chat.append(msg) + except: + pass + + def run(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + global chatlogger + self.timerr = babase.AppTimer(5.0, self.chatcheck, repeat=True) + + def chatcheck(self): + global unmuted_names + global cache_chat + try: + chats = bs.get_chat_messages() + except: + chats = [] + for msg in chats: + if msg in cache_chat: + pass + else: + if msg.split(":")[0] in unmuted_names: + bs.broadcastmessage(msg, color=(0.6, 0.9, 0.6)) + cache_chat.append(msg) + if len(self.saved_msg) > 45: + cache_chat.pop(0) + if babase.app.config.resolve('Chat Muted'): + pass + else: + self.timerr = None + + +def chatloggerstatus(): + global chatlogger + if chatlogger: + return "Turn off Chat Logger" + else: + return "Turn on chat logger" + + +def _getTransText(text, isBaLstr=False, same_fb=False): + global Current_Lang + global chatlogger + if Current_Lang != 'English': + Current_Lang = 'English' + global Language_Texts + Language_Texts = { + "Chinese": { + + }, + "English": { + "Add_a_Quick_Reply": "Add a Quick Reply", + "Admin_Command_Kick_Confirm": "Are you sure to use admin\ +command to kick %s?", + "Ban_For_%d_Seconds": "Ban for %d second(s).", + "Ban_Time_Post": "Enter the time you want to ban(Seconds).", + "Credits_for_This": "Credits for This", + "Custom_Action": "Custom Action", + "Debug_for_Host_Info": "Host Info Debug", + "Game_Record_Saved": "Game replay %s is saved.", + "Kick_ID": "Kick ID:%d", + "Mention_this_guy": "Mention this guy", + "Modify_Main_Color": "Modify Main Color", + "No_valid_player_found": "Can't find a valid player.", + "No_valid_player_id_found": "Can't find a valid player ID.", + "Normal_kick_confirm": "Are you sure to kick %s?", + "Remove_a_Quick_Reply": "Remove a Quick Reply", + "Restart_Game_Record": "Save Recording", + "Restart_Game_Record_Confirm": "Are you sure to restart recording game stream?", + "Send_%d_times": "Send for %d times", + "Something_is_added": "'%s' is added.", + "Something_is_removed": "'%s' is removed.", + "Times": "Times", + "Translator": "Translator", + "chatloggeroff": "Turn off Chat Logger", + "chatloggeron": "Turn on Chat Logger", + "screenmsgoff": "Hide ScreenMessage", + "screenmsgon": "Show ScreenMessage", + "unmutethisguy": "unmute this guy", + "mutethisguy": "mute this guy", + "muteall": "Mute all", + "unmuteall": "Unmute all", + "copymsg": "copy" + + } + } + + Language_Texts = Language_Texts.get(Current_Lang) + try: + from Language_Packs import ModifiedPartyWindow_LanguagePack as ext_lan_pack + if isinstance(ext_lan_pack, dict) and isinstance(ext_lan_pack.get(Current_Lang), dict): + complete_Pack = ext_lan_pack.get(Current_Lang) + for key, item in complete_Pack.items(): + Language_Texts[key] = item + except: + pass + + return (Language_Texts.get(text, "#Unknown Text#" if not same_fb else text) if not isBaLstr else + babase.Lstr(resource="??Unknown??", fallback_value=Language_Texts.get(text, "#Unknown Text#" if not same_fb else text))) + + +def _get_popup_window_scale() -> float: + uiscale = bui.app.ui_v1.uiscale + return (2.3 if uiscale is babase.UIScale.SMALL else + 1.65 if uiscale is babase.UIScale.MEDIUM else 1.23) + + +def _creat_Lstr_list(string_list: list = []) -> list: + return ([babase.Lstr(resource="??Unknown??", fallback_value=item) for item in string_list]) + + +customchatThread().run() + + +class ModifiedPartyWindow(bascenev1lib_party.PartyWindow): + def __init__(self, origin: Sequence[float] = (0, 0)): + if _babase.env().get("build_number") >= 22597: + self._uiopenstate = bui.UIOpenState('classicparty') + else: + bui.set_party_window_open(True) + self._r = 'partyWindow' + self.msg_user_selected = '' + self._popup_type: Optional[str] = None + self._popup_party_member_client_id: Optional[int] = None + self._popup_party_member_is_host: Optional[bool] = None + self._width = 500 + + uiscale = bui.app.ui_v1.uiscale + self._height = (365 if uiscale is babase.UIScale.SMALL else + 480 if uiscale is babase.UIScale.MEDIUM else 600) + + # Custom color here + self._bg_color = babase.app.config.get("PartyWindow_Main_Color", (0.40, 0.55, 0.20)) if not isinstance( + self._getCustomSets().get("Color"), (list, tuple)) else self._getCustomSets().get("Color") + if not isinstance(self._bg_color, (list, tuple)) or not len(self._bg_color) == 3: + self._bg_color = (0.40, 0.55, 0.20) + + bui.Window.__init__(self, root_widget=bui.containerwidget( + size=(self._width, self._height), + transition='in_scale', + color=self._bg_color, + parent=bui.get_special_widget('overlay_stack'), + on_outside_click_call=self.close_with_sound, + scale_origin_stack_offset=origin, + scale=(2.0 if uiscale is babase.UIScale.SMALL else + 1.35 if uiscale is babase.UIScale.MEDIUM else 1.0), + stack_offset=(0, -10) if uiscale is babase.UIScale.SMALL else ( + 240, 0) if uiscale is babase.UIScale.MEDIUM else (330, 20))) + + self._cancel_button = bui.buttonwidget(parent=self._root_widget, + scale=0.7, + position=(30, self._height - 47), + size=(50, 50), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.45, 0.63, 0.15), + icon=bui.gettexture('crossOut'), + iconscale=1.2) + self._smoothy_button = bui.buttonwidget(parent=self._root_widget, + scale=0.6, + position=(5, self._height - 47 - 40), + size=(50, 50), + label='69', + on_activate_call=self.smoothy_roster_changer, + autoselect=True, + color=(0.45, 0.63, 0.15), + icon=bui.gettexture('replayIcon'), + iconscale=1.2) + bui.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + + self._menu_button = bui.buttonwidget( + parent=self._root_widget, + scale=0.7, + position=(self._width - 60, self._height - 47), + size=(50, 50), + label="\xee\x80\x90", + autoselect=True, + button_type='square', + on_activate_call=bs.WeakCallStrict(self._on_menu_button_press), + color=(0.55, 0.73, 0.25), + icon=bui.gettexture('menuButton'), + iconscale=1.2) + + info = bs.get_connection_to_host_info_2() + if info != None: + title = info.name + else: + title = babase.Lstr(resource=self._r + '.titleText') + + self._title_text = bui.textwidget(parent=self._root_widget, + scale=0.9, + color=(0.5, 0.7, 0.5), + text=title, + size=(120, 20), + position=(self._width * 0.5-60, + self._height - 29), + on_select_call=self.title_selected, + selectable=True, + maxwidth=self._width * 0.7, + h_align='center', + v_align='center') + + self._empty_str = bui.textwidget(parent=self._root_widget, + scale=0.75, + size=(0, 0), + position=(self._width * 0.5, + self._height - 65), + maxwidth=self._width * 0.85, + text="no one", + h_align='center', + v_align='center') + + self._scroll_width = self._width - 50 + self._scrollwidget = bui.scrollwidget(parent=self._root_widget, + size=(self._scroll_width, + self._height - 200), + position=(30, 80), + color=(0.4, 0.6, 0.3)) + self._columnwidget = bui.columnwidget(parent=self._scrollwidget, + border=2, + left_border=-200, + margin=0) + bui.widget(edit=self._menu_button, down_widget=self._columnwidget) + + self._muted_text = bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + size=(0, 0), + h_align='center', + v_align='center', + text="") + self._chat_texts: List[bui.Widget] = [] + self._chat_texts_haxx: List[bui.Widget] = [] + + # add all existing messages if chat is not muted + # print("updates") + if True: # smoothy - always show chat in partywindow + msgs = bs.get_chat_messages() + for msg in msgs: + self._add_msg(msg) + # print(msg) + # else: + # msgs=_babase.get_chat_messages() + # for msg in msgs: + # print(msg); + # txt = bui.textwidget(parent=self._columnwidget, + # text=msg, + # h_align='left', + # v_align='center', + # size=(0, 13), + # scale=0.55, + # maxwidth=self._scroll_width * 0.94, + # shadow=0.3, + # flatness=1.0) + # self._chat_texts.append(txt) + # if len(self._chat_texts) > 40: + # first = self._chat_texts.pop(0) + # first.delete() + # bui.containerwidget(edit=self._columnwidget, visible_child=txt) + self.ping_widget = txt = bui.textwidget( + parent=self._root_widget, + scale=0.6, + size=(20, 5), + color=(0.45, 0.63, 0.15), + position=(self._width/2 - 20, 50), + text="Ping:"+str(current_ping)+" ms", + selectable=True, + autoselect=False, + v_align='center') + _babase.ping_widget = self.ping_widget + + def enable_chat_mode(): + pass + + self._text_field = txt = bui.textwidget( + parent=self._root_widget, + editable=True, + size=(530-80, 40), + position=(44+60, 39), + text='', + maxwidth=494, + shadow=0.3, + flatness=1.0, + description=babase.Lstr(resource=self._r + '.chatMessageText'), + autoselect=True, + v_align='center', + corner_scale=0.7) + + # for m in _babase.get_chat_messages(): + # if m: + # ttchat=bui.textwidget( + # parent=self._columnwidget, + # size=(10,10), + # h_align='left', + # v_align='center', + # text=str(m), + # scale=0.6, + # flatness=0, + # color=(2,2,2), + # shadow=0, + # always_highlight=True + + # ) + bui.widget(edit=self._scrollwidget, + autoselect=True, + left_widget=self._cancel_button, + up_widget=self._cancel_button, + down_widget=self._text_field) + bui.widget(edit=self._columnwidget, + autoselect=True, + up_widget=self._cancel_button, + down_widget=self._text_field) + bui.containerwidget(edit=self._root_widget, selected_child=txt) + btn = bui.buttonwidget(parent=self._root_widget, + size=(50, 35), + label=babase.Lstr(resource=self._r + '.sendText'), + button_type='square', + autoselect=True, + position=(self._width - 70, 35), + on_activate_call=self._send_chat_message) + + def _times_button_on_click(): + # self._popup_type = "send_Times_Press" + # allow_range = 100 if _babase.get_foreground_host_session() is not None else 4 + # PopupMenuWindow(position=self._times_button.get_screen_space_center(), + # scale=_get_popup_window_scale(), + # choices=[str(index) for index in range(1,allow_range + 1)], + # choices_display=_creat_Lstr_list([_getTransText("Send_%d_times")%int(index) for index in range(1,allow_range + 1)]), + # current_choice="Share_Server_Info", + # delegate=self) + Quickreply = self._get_quick_responds() + if len(Quickreply) > 0: + PopupMenuWindow(position=self._times_button.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=Quickreply, + choices_display=_creat_Lstr_list(Quickreply), + current_choice=Quickreply[0], + delegate=self) + self._popup_type = "QuickMessageSelect" + + self._send_msg_times = 1 + + self._times_button = bui.buttonwidget(parent=self._root_widget, + size=(50, 35), + label="Quick", + button_type='square', + autoselect=True, + position=(30, 35), + on_activate_call=_times_button_on_click) + + bui.textwidget(edit=txt, on_return_press_call=btn.activate) + self._name_widgets: List[bui.Widget] = [] + self._roster: Optional[List[Dict[str, Any]]] = None + + self.smoothy_mode = 1 + self.full_chat_mode = False + self._update_timer = babase.AppTimer(1.0, + bs.WeakCallStrict(self._update), + repeat=True) + + self._update() + + def title_selected(self): + + self.full_chat_mode = self.full_chat_mode == False + self._update() + + def smoothy_roster_changer(self): + + self.smoothy_mode = (self.smoothy_mode+1) % 3 + + self._update() + + def on_chat_message(self, msg: str) -> None: + """Called when a new chat message comes through.""" + # print("on_chat"+msg) + if True: + self._add_msg(msg) + + def _copy_msg(self, msg: str) -> None: + if bui.clipboard_is_supported(): + bui.clipboard_set_text(msg) + bui.screenmessage( + bui.Lstr(resource='copyConfirmText'), + color=(0, 1, 0) + ) + + def _on_chat_press(self, msg, widget, showMute): + global unmuted_names + choices = ['copy'] + choices_display = [_getTransText("copymsg", isBaLstr=True)] + if showMute: + if msg.split(":")[0].encode('utf-8') in unmuted_names: + choices.append('mute') + choices_display.append(_getTransText("mutethisguy", isBaLstr=True)) + else: + choices.append('unmute') + choices_display.append(_getTransText("unmutethisguy", isBaLstr=True)) + PopupMenuWindow(position=widget.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=choices, + choices_display=choices_display, + current_choice="@ this guy", + delegate=self) + self.msg_user_selected = msg + self._popup_type = "chatmessagepress" + + # bs.chatmessage("pressed") + + def _add_msg(self, msg: str) -> None: + try: + showMute = babase.app.config.resolve('Chat Muted') + if True: + txt = bui.textwidget(parent=self._columnwidget, + text=msg, + h_align='left', + v_align='center', + size=(900, 13), + scale=0.55, + position=(-0.6, 0), + selectable=True, + autoselect=True, + click_activate=True, + maxwidth=self._scroll_width * 0.94, + shadow=0.3, + flatness=1.0) + bui.textwidget(edit=txt, + on_activate_call=babase.CallPartial( + self._on_chat_press, + msg, txt, showMute)) + + # btn = bui.buttonwidget(parent=self._columnwidget, + # scale=0.7, + # size=(100,20), + # label="smoothy buttin", + # icon=bs.gettexture('replayIcon'), + # texture=None, + # ) + self._chat_texts_haxx.append(txt) + if len(self._chat_texts_haxx) > 40: + first = self._chat_texts_haxx.pop(0) + first.delete() + bui.containerwidget(edit=self._columnwidget, visible_child=txt) + except Exception: + pass + + def _add_msg_when_muted(self, msg: str) -> None: + + txt = bui.textwidget(parent=self._columnwidget, + text=msg, + h_align='left', + v_align='center', + size=(0, 13), + scale=0.55, + maxwidth=self._scroll_width * 0.94, + shadow=0.3, + flatness=1.0) + self._chat_texts.append(txt) + if len(self._chat_texts) > 40: + first = self._chat_texts.pop(0) + first.delete() + bui.containerwidget(edit=self._columnwidget, visible_child=txt) + + def color_picker_closing(self, picker) -> None: + babase._appconfig.commit_app_config() + + def color_picker_selected_color(self, picker, color) -> None: + # bs.animateArray(self._root_widget,"color",3,{0:self._bg_color,1500:color}) + bui.containerwidget(edit=self._root_widget, color=color) + self._bg_color = color + babase.app.config["PartyWindow_Main_Color"] = color + + def _on_nick_rename_press(self, arg) -> None: + + bui.containerwidget(edit=self._root_widget, transition='out_scale') + c_width = 600 + c_height = 250 + uiscale = bui.app.ui_v1.uiscale + self._nick_rename_window = cnt = bui.containerwidget( + + scale=(1.8 if uiscale is babase.UIScale.SMALL else + 1.55 if uiscale is babase.UIScale.MEDIUM else 1.0), + size=(c_width, c_height), + transition='in_scale') + + bui.textwidget(parent=cnt, + size=(0, 0), + h_align='center', + v_align='center', + text='Enter nickname', + maxwidth=c_width * 0.8, + position=(c_width * 0.5, c_height - 60)) + id = self._get_nick(arg) + self._player_nick_text = txt89 = bui.textwidget( + parent=cnt, + size=(c_width * 0.8, 40), + h_align='left', + v_align='center', + text=id, + editable=True, + description='Players nick name', + position=(c_width * 0.1, c_height - 140), + autoselect=True, + maxwidth=c_width * 0.7, + max_chars=200) + cbtn = bui.buttonwidget( + parent=cnt, + label=babase.Lstr(resource='cancelText'), + on_activate_call=babase.CallPartial( + lambda c: bui.containerwidget(edit=c, transition='out_scale'), + cnt), + size=(180, 60), + position=(30, 30), + autoselect=True) + okb = bui.buttonwidget(parent=cnt, + label='Rename', + size=(180, 60), + position=(c_width - 230, 30), + on_activate_call=babase.CallPartial( + self._add_nick, arg), + autoselect=True) + bui.widget(edit=cbtn, right_widget=okb) + bui.widget(edit=okb, left_widget=cbtn) + bui.textwidget(edit=txt89, on_return_press_call=okb.activate) + bui.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) + + def _add_nick(self, arg): + config = babase.app.config + new_name_raw = cast(str, bui.textwidget(query=self._player_nick_text)) + if arg: + if not isinstance(config.get('players nick'), dict): + config['players nick'] = {} + config['players nick'][arg] = new_name_raw + config.commit() + bui.containerwidget(edit=self._nick_rename_window, + transition='out_scale') + # bui.containerwidget(edit=self._root_widget,transition='in_scale') + + def _get_nick(self, id): + config = babase.app.config + if not isinstance(config.get('players nick'), dict): + return "add nick" + elif id in config['players nick']: + return config['players nick'][id] + else: + return "add nick" + + def _reset_game_record(self) -> None: + try: + dir_path = _babase.get_replays_dir() + curFilePath = os.path.join(dir_path+os.sep, "__lastReplay.brp").encode(SystemEncode) + newFileName = str(babase.Lstr(resource="replayNameDefaultText").evaluate( + )+" (%s)" % (datetime.datetime.strftime(datetime.datetime.now(), "%Y_%m_%d_%H_%M_%S"))+".brp") + newFilePath = os.path.join(dir_path+os.sep, newFileName).encode(SystemEncode) + # print(curFilePath, newFilePath) + # os.rename(curFilePath,newFilePath) + shutil.copyfile(curFilePath, newFilePath) + bs.broadcastmessage(_getTransText("Game_Record_Saved") % newFileName, color=(1, 1, 1)) + except: + babase.print_exception() + bs.broadcastmessage(babase.Lstr(resource="replayWriteErrorText").evaluate() + + ""+traceback.format_exc(), color=(1, 0, 0)) + + def _on_menu_button_press(self) -> None: + is_muted = babase.app.config.resolve('Chat Muted') + global chatlogger + choices = ["unmute" if is_muted else "mute", "screenmsg", + "addQuickReply", "removeQuickReply", "chatlogger", "credits"] + DisChoices = [_getTransText("unmuteall", isBaLstr=True) if is_muted else _getTransText("muteall", isBaLstr=True), + _getTransText("screenmsgoff", isBaLstr=True) if screenmsg else _getTransText( + "screenmsgon", isBaLstr=True), + + _getTransText("Add_a_Quick_Reply", isBaLstr=True), + _getTransText("Remove_a_Quick_Reply", isBaLstr=True), + _getTransText("chatloggeroff", isBaLstr=True) if chatlogger else _getTransText( + "chatloggeron", isBaLstr=True), + _getTransText("Credits_for_This", isBaLstr=True) + ] + + choices.append("resetGameRecord") + DisChoices.append(_getTransText("Restart_Game_Record", isBaLstr=True)) + if self._getCustomSets().get("Enable_HostInfo_Debug", False): + choices.append("hostInfo_Debug") + DisChoices.append(_getTransText("Debug_for_Host_Info", isBaLstr=True)) + + PopupMenuWindow( + position=self._menu_button.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=choices, + choices_display=DisChoices, + current_choice="unmute" if is_muted else "mute", delegate=self) + self._popup_type = "menu" + + def _on_party_member_press(self, client_id: int, is_host: bool, + widget: bui.Widget) -> None: + # if we"re the host, pop up "kick" options for all non-host members + if bs.get_foreground_host_session() is not None: + kick_str = babase.Lstr(resource="kickText") + else: + kick_str = babase.Lstr(resource="kickVoteText") + choices = ["kick", "@ this guy", "info", "adminkick"] + + choices_display = [kick_str, _getTransText("Mention_this_guy", isBaLstr=True), babase.Lstr(resource="??Unknown??", fallback_value="Info"), + babase.Lstr(resource="??Unknown??", fallback_value=_getTransText("Kick_ID") % client_id)] + + try: + if len(self._getCustomSets().get("partyMemberPress_Custom") if isinstance(self._getCustomSets().get("partyMemberPress_Custom"), dict) else {}) > 0: + choices.append("customAction") + choices_display.append(_getTransText("Custom_Action", isBaLstr=True)) + except: + babase.print_exception() + + PopupMenuWindow(position=widget.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=choices, + choices_display=choices_display, + current_choice="@ this guy", + delegate=self) + self._popup_party_member_client_id = client_id + self._popup_party_member_is_host = is_host + self._popup_type = "partyMemberPress" + + def _send_chat_message(self) -> None: + sendtext = bui.textwidget(query=self._text_field) + if sendtext == ".ip": + bs.chatmessage("IP "+ip_add+" PORT "+str(p_port)) + + bui.textwidget(edit=self._text_field, text="") + return + elif sendtext == ".info": + if bs.get_connection_to_host_info_2() == None: + s_build = 0 + else: + s_build = bs.get_connection_to_host_info_2()['build_number'] + s_v = "0" + if s_build <= 14365: + s_v = " 1.4.148 or below" + elif s_build <= 14377: + s_v = "1.4.148 < x < = 1.4.155 " + elif s_build >= 20001 and s_build < 20308: + s_v = "1.5" + elif s_build >= 20308 and s_build < 20591: + s_v = "1.6 " + else: + s_v = "1.7 and above " + bs.chatmessage("script version "+s_v+"- build "+str(s_build)) + bui.textwidget(edit=self._text_field, text="") + return + elif sendtext == ".ping": + bs.chatmessage("My ping:"+str(current_ping)) + bui.textwidget(edit=self._text_field, text="") + return + elif sendtext == ".save": + info = bs.get_connection_to_host_info_2() + config = babase.app.config + if info != None and info.get('name', '') != '': + title = info['name'] + if not isinstance(config.get('Saved Servers'), dict): + config['Saved Servers'] = {} + config['Saved Servers'][f'{ip_add}@{p_port}'] = { + 'addr': ip_add, + 'port': p_port, + 'name': title + } + config.commit() + bs.broadcastmessage("Server saved to manual") + bui.getsound('gunCocking').play() + bui.textwidget(edit=self._text_field, text="") + return + # elif sendtext != "": + # for index in range(getattr(self,"_send_msg_times",1)): + if '\\' in sendtext: + sendtext = sendtext.replace('\\d', ('\ue048')) + sendtext = sendtext.replace('\\c', ('\ue043')) + sendtext = sendtext.replace('\\h', ('\ue049')) + sendtext = sendtext.replace('\\s', ('\ue046')) + sendtext = sendtext.replace('\\n', ('\ue04b')) + sendtext = sendtext.replace('\\f', ('\ue04f')) + sendtext = sendtext.replace('\\g', ('\ue027')) + sendtext = sendtext.replace('\\i', ('\ue03a')) + sendtext = sendtext.replace('\\m', ('\ue04d')) + sendtext = sendtext.replace('\\t', ('\ue01f')) + sendtext = sendtext.replace('\\bs', ('\ue01e')) + sendtext = sendtext.replace('\\j', ('\ue010')) + sendtext = sendtext.replace('\\e', ('\ue045')) + sendtext = sendtext.replace('\\l', ('\ue047')) + sendtext = sendtext.replace('\\a', ('\ue020')) + sendtext = sendtext.replace('\\b', ('\ue00c')) + if sendtext == "": + sendtext = " " + msg = sendtext + msg1 = msg.split(" ") + ms2 = "" + if (len(msg1) > 11): + hp = int(len(msg1)/2) + + for m in range(0, hp): + ms2 = ms2+" "+msg1[m] + + bs.chatmessage(ms2) + + ms2 = "" + for m in range(hp, len(msg1)): + ms2 = ms2+" "+msg1[m] + bs.chatmessage(ms2) + else: + bs.chatmessage(msg) + + bui.textwidget(edit=self._text_field, text="") + # else: + # Quickreply = self._get_quick_responds() + # if len(Quickreply) > 0: + # PopupMenuWindow(position=self._text_field.get_screen_space_center(), + # scale=_get_popup_window_scale(), + # choices=Quickreply, + # choices_display=_creat_Lstr_list(Quickreply), + # current_choice=Quickreply[0], + # delegate=self) + # self._popup_type = "QuickMessageSelect" + # else: + # bs.chatmessage(sendtext) + # bui.textwidget(edit=self._text_field,text="") + + def _get_quick_responds(self): + if not hasattr(self, "_caches") or not isinstance(self._caches, dict): + self._caches = {} + try: + filePath = os.path.join(RecordFilesDir, "Quickmessage.txt") + + if os.path.exists(RecordFilesDir) is not True: + os.makedirs(RecordFilesDir) + + if not os.path.isfile(filePath): + with open(filePath, "wb") as writer: + writer.write(({"Chinese": u"\xe5\x8e\x89\xe5\xae\xb3\xef\xbc\x8c\xe8\xbf\x98\xe6\x9c\x89\xe8\xbf\x99\xe7\xa7\x8d\xe9\xaa\x9a\xe6\x93\x8d\xe4\xbd\x9c!\ +\xe4\xbd\xa0\xe2\x84\xa2\xe8\x83\xbd\xe5\x88\xab\xe6\x89\x93\xe9\x98\x9f\xe5\x8f\x8b\xe5\x90\x97\xef\xbc\x9f\ +\xe5\x8f\xaf\xe4\xbb\xa5\xe5\x95\x8a\xe5\xb1\x85\xe7\x84\xb6\xe8\x83\xbd\xe8\xbf\x99\xe4\xb9\x88\xe7\x8e\xa9\xef\xbc\x9f"}.get(Current_Lang, "Thats Amazing !")).encode("UTF-8")) + if os.path.getmtime(filePath) != self._caches.get("Vertify_Quickresponse_Text"): + with open(filePath, "r+", encoding="utf-8") as Reader: + Text = Reader.read() + if Text.startswith(str(codecs.BOM_UTF8)): + Text = Text[3:] + self._caches["quickReplys"] = (Text).split("\\n") + self._caches["Vertify_Quickresponse_Text"] = os.path.getmtime(filePath) + return (self._caches.get("quickReplys", [])) + except: + babase.print_exception() + bs.broadcastmessage(babase.Lstr(resource="errorText"), (1, 0, 0)) + bui.getsound("error").play() + + def _write_quick_responds(self, data): + try: + with open(os.path.join(RecordFilesDir, "Quickmessage.txt"), "wb") as writer: + writer.write("\\n".join(data).encode("utf-8")) + except: + babase.print_exception() + bs.broadcastmessage(babase.Lstr(resource="errorText"), (1, 0, 0)) + bui.getsound("error").play() + + def _getCustomSets(self): + try: + if not hasattr(self, "_caches") or not isinstance(self._caches, dict): + self._caches = {} + try: + from VirtualHost import MainSettings + if MainSettings.get("Custom_PartyWindow_Sets", {}) != self._caches.get("PartyWindow_Sets", {}): + self._caches["PartyWindow_Sets"] = MainSettings.get( + "Custom_PartyWindow_Sets", {}) + except: + try: + filePath = os.path.join(RecordFilesDir, "Settings.json") + if os.path.isfile(filePath): + if os.path.getmtime(filePath) != self._caches.get("Vertify_MainSettings.json_Text"): + with open(filePath, "r+", encoding="utf-8") as Reader: + Text = Reader.read() + if Text.startswith(str(codecs.BOM_UTF8)): + Text = Text[3:] + self._caches["PartyWindow_Sets"] = json.loads( + Text.decode("utf-8")).get("Custom_PartyWindow_Sets", {}) + self._caches["Vertify_MainSettings.json_Text"] = os.path.getmtime( + filePath) + except: + babase.print_exception() + return (self._caches.get("PartyWindow_Sets") if isinstance(self._caches.get("PartyWindow_Sets"), dict) else {}) + + except: + babase.print_exception() + + def _getObjectByID(self, type="playerName", ID=None): + if ID is None: + ID = self._popup_party_member_client_id + type = type.lower() + output = [] + for roster in self._roster: + if type.startswith("all"): + if type in ("roster", "fullrecord"): + output += [roster] + elif type.find("player") != -1 and roster["players"] != []: + if type.find("namefull") != -1: + output += [(i["name_full"]) for i in roster["players"]] + elif type.find("name") != -1: + output += [(i["name"]) for i in roster["players"]] + elif type.find("playerid") != -1: + output += [i["id"] for i in roster["players"]] + elif type.lower() in ("account", "displaystring"): + output += [(roster["display_string"])] + elif roster["client_id"] == ID and not type.startswith("all"): + try: + if type in ("roster", "fullrecord"): + return (roster) + elif type.find("player") != -1 and roster["players"] != []: + if len(roster["players"]) == 1 or type.find("singleplayer") != -1: + if type.find("namefull") != -1: + return ((roster["players"][0]["name_full"])) + elif type.find("name") != -1: + return ((roster["players"][0]["name"])) + elif type.find("playerid") != -1: + return (roster["players"][0]["id"]) + else: + if type.find("namefull") != -1: + return ([(i["name_full"]) for i in roster["players"]]) + elif type.find("name") != -1: + return ([(i["name"]) for i in roster["players"]]) + elif type.find("playerid") != -1: + return ([i["id"] for i in roster["players"]]) + elif type.lower() in ("account", "displaystring"): + return ((roster["display_string"])) + except: + babase.print_exception() + + return (None if len(output) == 0 else output) + + def _edit_text_msg_box(self, text, type="rewrite"): + if not isinstance(type, str) or not isinstance(text, str): + return + type = type.lower() + text = (text) + if type.find("add") != -1: + bui.textwidget(edit=self._text_field, text=bui.textwidget(query=self._text_field)+text) + else: + bui.textwidget(edit=self._text_field, text=text) + + def _send_admin_kick_command(self): bs.chatmessage( + "/kick " + str(self._popup_party_member_client_id)) + + def new_input_window_callback(self, got_text, flag, code): + if got_text: + if flag.startswith("Host_Kick_Player:"): + try: + result = _babase.disconnect_client( + self._popup_party_member_client_id, ban_time=int(code)) + if not result: + bui.getsound('error').play() + bs.broadcastmessage( + babase.Lstr(resource='getTicketsWindow.unavailableText'), + color=(1, 0, 0)) + except: + bui.getsound('error').play() + print(traceback.format_exc()) + + def _kick_selected_player(self): + """ + result = _babase._disconnectClient(self._popup_party_member_client_id,banTime) + if not result: + bs.getsound("error").play() + bs.broadcastmessage(babase.Lstr(resource="getTicketsWindow.unavailableText"),color=(1,0,0)) + """ + if self._popup_party_member_client_id != -1: + if bs.get_foreground_host_session() is not None: + self._popup_type = "banTimePress" + choices = [0, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200, 99999999] if not (isinstance(self._getCustomSets().get("Ban_Time_List"), list) + and all([isinstance(item, int) for item in self._getCustomSets().get("Ban_Time_List")])) else self._getCustomSets().get("Ban_Time_List") + PopupMenuWindow(position=self.get_root_widget().get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=[str(item) for item in choices], + choices_display=_creat_Lstr_list( + [_getTransText("Ban_For_%d_Seconds") % item for item in choices]), + current_choice="Share_Server_Info", + delegate=self) + """ + NewInputWindow(origin_widget = self.get_root_widget(), + delegate = self,post_text = _getTransText("Ban_Time_Post"), + default_code = "300",flag = "Host_Kick_Player:"+str(self._popup_party_member_client_id)) + """ + else: + # kick-votes appeared in build 14248 + info = bs.get_connection_to_host_info_2() + if bool(info) and (info.build_number < + 14248): + bui.getsound('error').play() + bs.broadcastmessage( + babase.Lstr(resource='getTicketsWindow.unavailableText'), + color=(1, 0, 0)) + else: + + # Ban for 5 minutes. + result = bs.disconnect_client( + self._popup_party_member_client_id, ban_time=5 * 60) + if not result: + bui.getsound('error').play() + bs.broadcastmessage( + babase.Lstr(resource='getTicketsWindow.unavailableText'), + color=(1, 0, 0)) + else: + bui.getsound('error').play() + bs.broadcastmessage( + babase.Lstr(resource='internal.cantKickHostError'), + color=(1, 0, 0)) + + # NewShareCodeWindow(origin_widget=self.get_root_widget(), delegate=None,code = "300",execText = u"_babase._disconnectClient(%d,{Value})"%self._popup_party_member_client_id) + def joinbombspot(self): + + bui.open_url("https://discord.gg/ucyaesh") + + def _update(self) -> None: + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-nested-blocks + + # # update muted state + # if babase.app.config.resolve('Chat Muted'): + # bui.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.3)) + # # clear any chat texts we're showing + # if self._chat_texts: + # while self._chat_texts: + # first = self._chat_texts.pop() + # first.delete() + # else: + # bui.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0)) + + # update roster section + + roster = bs.get_game_roster() + global f_chat + global smo_mode + if roster != self._roster or smo_mode != self.smoothy_mode or f_chat != self.full_chat_mode: + self._roster = roster + smo_mode = self.smoothy_mode + f_chat = self.full_chat_mode + # clear out old + for widget in self._name_widgets: + widget.delete() + self._name_widgets = [] + + if not self._roster: + top_section_height = 60 + bui.textwidget(edit=self._empty_str, + text=babase.Lstr(resource=self._r + '.emptyText')) + bui.scrollwidget(edit=self._scrollwidget, + size=(self._width - 50, + self._height - top_section_height - 110), + position=(30, 80)) + elif self.full_chat_mode: + top_section_height = 60 + bui.scrollwidget(edit=self._scrollwidget, + size=(self._width - 50, + self._height - top_section_height - 75), + position=(30, 80)) + + else: + columns = 1 if len( + self._roster) == 1 else 2 if len(self._roster) == 2 else 3 + rows = int(math.ceil(float(len(self._roster)) / columns)) + c_width = (self._width * 0.9) / max(3, columns) + c_width_total = c_width * columns + c_height = 24 + c_height_total = c_height * rows + for y in range(rows): + for x in range(columns): + index = y * columns + x + if index < len(self._roster): + t_scale = 0.65 + pos = (self._width * 0.53 - c_width_total * 0.5 + + c_width * x - 23, + self._height - 65 - c_height * y - 15) + + # if there are players present for this client, use + # their names as a display string instead of the + # client spec-string + try: + if self.smoothy_mode == 1 and self._roster[index]['players']: + # if there's just one, use the full name; + # otherwise combine short names + if len(self._roster[index] + ['players']) == 1: + p_str = self._roster[index]['players'][ + 0]['name_full'] + else: + p_str = ('/'.join([ + entry['name'] for entry in + self._roster[index]['players'] + ])) + if len(p_str) > 25: + p_str = p_str[:25] + '...' + elif self.smoothy_mode == 0: + p_str = self._roster[index][ + 'display_string'] + p_str = self._get_nick(p_str) + + else: + p_str = self._roster[index][ + 'display_string'] + + except Exception: + babase.print_exception( + 'Error calcing client name str.') + p_str = '???' + try: + widget = bui.textwidget(parent=self._root_widget, + position=(pos[0], pos[1]), + scale=t_scale, + size=(c_width * 0.85, 30), + maxwidth=c_width * 0.85, + color=(1, 1, + 1) if index == 0 else + (1, 1, 1), + selectable=True, + autoselect=True, + click_activate=True, + text=babase.Lstr(value=p_str), + h_align='left', + v_align='center') + self._name_widgets.append(widget) + except Exception: + pass + # in newer versions client_id will be present and + # we can use that to determine who the host is. + # in older versions we assume the first client is + # host + if self._roster[index]['client_id'] is not None: + is_host = self._roster[index][ + 'client_id'] == -1 + else: + is_host = (index == 0) + + # FIXME: Should pass client_id to these sort of + # calls; not spec-string (perhaps should wait till + # client_id is more readily available though). + try: + bui.textwidget(edit=widget, + on_activate_call=babase.CallPartial( + self._on_party_member_press, + self._roster[index]['client_id'], + is_host, widget)) + except Exception: + pass + pos = (self._width * 0.53 - c_width_total * 0.5 + + c_width * x, + self._height - 65 - c_height * y) + + # Make the assumption that the first roster + # entry is the server. + # FIXME: Shouldn't do this. + if is_host: + twd = min( + c_width * 0.85, + _babase.get_string_width( + p_str, suppress_warning=True) * + t_scale) + try: + self._name_widgets.append( + bui.textwidget( + parent=self._root_widget, + position=(pos[0] + twd + 1, + pos[1] - 0.5), + size=(0, 0), + h_align='left', + v_align='center', + maxwidth=c_width * 0.96 - twd, + color=(0.1, 1, 0.1, 0.5), + text=babase.Lstr(resource=self._r + + '.hostText'), + scale=0.4, + shadow=0.1, + flatness=1.0)) + except Exception: + pass + try: + bui.textwidget(edit=self._empty_str, text='') + bui.scrollwidget(edit=self._scrollwidget, + size=(self._width - 50, + max(100, self._height - 139 - + c_height_total)), + position=(30, 80)) + except Exception: + pass + + def hide_screen_msg(self): + file = open('ba_data/data/languages/english.json') + eng = json.loads(file.read()) + file.close() + eng['internal']['playerJoinedPartyText'] = '' + eng['internal']['playerLeftPartyText'] = '' + eng['internal']['chatBlockedText'] = '' + eng['kickVoteStartedText'] = '' + # eng['kickVoteText']='' + eng['kickWithChatText'] = '' + eng['kickOccurredText'] = '' + eng['kickVoteFailedText'] = '' + eng['votesNeededText'] = '' + eng['playerDelayedJoinText'] = '' + eng['playerLeftText'] = '' + eng['kickQuestionText'] = '' + file = open('ba_data/data/languages/english.json', "w") + json.dump(eng, file) + file.close() + bs.app.lang.setlanguage(None) + + def restore_screen_msg(self): + file = open('ba_data/data/languages/english.json') + eng = json.loads(file.read()) + file.close() + eng['internal']['playerJoinedPartyText'] = "${NAME} joined the pawri!" + eng['internal']['playerLeftPartyText'] = "${NAME} left the pawri." + eng['internal']['chatBlockedText'] = "${NAME} is chat-blocked for ${TIME} seconds." + eng['kickVoteStartedText'] = "A kick vote has been started for ${NAME}." + # eng['kickVoteText']='' + eng['kickWithChatText'] = "Type ${YES} in chat for yes and ${NO} for no." + eng['kickOccurredText'] = "${NAME} was kicked." + eng['kickVoteFailedText'] = "Kick-vote failed." + eng['votesNeededText'] = "${NUMBER} votes needed" + eng['playerDelayedJoinText'] = "${PLAYER} will enter at the start of the next round." + eng['playerLeftText'] = "${PLAYER} left the game." + eng['kickQuestionText'] = "Kick ${NAME}?" + file = open('ba_data/data/languages/english.json', "w") + json.dump(eng, file) + file.close() + bs.app.lang.setlanguage(None) + def popup_menu_selected_choice(self, popup_window: PopupMenuWindow, + choice: str) -> None: + """Called when a choice is selected in the popup.""" + global unmuted_names + if self._popup_type == "banTimePress": + result = _babase.disconnect_client( + self._popup_party_member_client_id, ban_time=int(choice)) + if not result: + bui.getsound('error').play() + bs.broadcastmessage( + babase.Lstr(resource='getTicketsWindow.unavailableText'), + color=(1, 0, 0)) + elif self._popup_type == "send_Times_Press": + self._send_msg_times = int(choice) + bui.buttonwidget(edit=self._times_button, label="%s:%d" % + (_getTransText("Times"), getattr(self, "_send_msg_times", 1))) + + elif self._popup_type == "chatmessagepress": + if choice == "mute": + unmuted_names.remove(self.msg_user_selected.split(":")[0].encode('utf-8')) + if choice == "unmute": + unmuted_names.append(self.msg_user_selected.split(":")[0].encode('utf-8')) + if choice == "copy": + self._copy_msg(self.msg_user_selected) + + elif self._popup_type == "partyMemberPress": + if choice == "kick": + ConfirmWindow(text=_getTransText("Normal_kick_confirm") % self._getObjectByID("account"), + action=self._kick_selected_player, cancel_button=True, cancel_is_selected=True, + color=self._bg_color, text_scale=1.0, + origin_widget=self.get_root_widget()) + elif choice == "info": + account = self._getObjectByID("account") + + self.loading_widget = ConfirmWindow(text="Searching .....", + color=self._bg_color, text_scale=1.0, cancel_button=False, + origin_widget=self.get_root_widget()) + start_new_thread(fetchAccountInfo, (account, self.loading_widget,)) + + elif choice == "adminkick": + ConfirmWindow(text=_getTransText("Admin_Command_Kick_Confirm") % self._getObjectByID("account"), + action=self._send_admin_kick_command, cancel_button=True, cancel_is_selected=True, + color=self._bg_color, text_scale=1.0, + origin_widget=self.get_root_widget()) + + elif choice == "@ this guy": + ChoiceDis = [] + NewChoices = [] + account = self._getObjectByID("account") + ChoiceDis.append(account) + temp = self._getObjectByID("playerNameFull") + if temp is not None: + if isinstance(temp, str) and temp not in ChoiceDis: + ChoiceDis.append(temp) + elif isinstance(temp, (list, tuple)): + for item in temp: + if isinstance(item, str) and item not in ChoiceDis: + ChoiceDis.append(item) + # print("r\\"" + + for item in ChoiceDis: + NewChoices.append(u"self._edit_text_msg_box('%s','add')" % + (item.replace("'", r"'").replace('"', r'\\"'))) + + else: + nick = self._get_nick(account) + ChoiceDis.append(nick) + NewChoices.append(u"self._on_nick_rename_press('%s')" % (account)) + p = PopupMenuWindow(position=popup_window.root_widget.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=NewChoices, + choices_display=_creat_Lstr_list(ChoiceDis), + current_choice=NewChoices[0], + delegate=self) + self._popup_type = "Custom_Exec_Choice" + elif choice == "customAction": + customActionSets = self._getCustomSets() + customActionSets = customActionSets.get("partyMemberPress_Custom") if isinstance( + customActionSets.get("partyMemberPress_Custom"), dict) else {} + ChoiceDis = [] + NewChoices = [] + for key, item in customActionSets.items(): + ChoiceDis.append(key) + NewChoices.append(item) + if len(ChoiceDis) > 0: + p = PopupMenuWindow(position=popup_window.root_widget.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=NewChoices, + choices_display=_creat_Lstr_list(ChoiceDis), + current_choice=NewChoices[0], + delegate=self) + self._popup_type = "customAction_partyMemberPress" + else: + bui.getsound("error").play() + bs.broadcastmessage( + babase.Lstr(resource="getTicketsWindow.unavailableText"), color=(1, 0, 0)) + elif self._popup_type == "menu": + if choice in ("mute", "unmute"): + cfg = babase.app.config + cfg['Chat Muted'] = (choice == 'mute') + cfg.apply_and_commit() + if cfg['Chat Muted']: + customchatThread().run() + self._update() + elif choice in ("credits",): + ConfirmWindow(text="AdvancePartyWindow by Mr.Smoothy \n extended version of ModifyPartyWindow(Plasma Boson) \n Version 5.3 \n Dont modify or release the source code \n Discord : \n mr.smoothy#5824 Plasma Boson#4104", + action=self.joinbombspot, width=420, height=200, + cancel_button=False, cancel_is_selected=False, + color=self._bg_color, text_scale=1.0, ok_text="More mods >", cancel_text=None, + origin_widget=self.get_root_widget()) + elif choice == "chatlogger": + # ColorPickerExact(parent=self.get_root_widget(), position=self.get_root_widget().get_screen_space_center(), + # initial_color=self._bg_color, delegate=self, tag='') + global chatlogger + if chatlogger: + chatlogger = False + bs.broadcastmessage("Chat logger turned OFF") + else: + chatlogger = True + chatloggThread().run() + bs.broadcastmessage("Chat logger turned ON") + elif choice == 'screenmsg': + global screenmsg + if screenmsg: + screenmsg = False + self.hide_screen_msg() + else: + screenmsg = True + self.restore_screen_msg() + elif choice == "addQuickReply": + try: + newReply = bui.textwidget(query=self._text_field) + data = self._get_quick_responds() + data.append(newReply) + self._write_quick_responds(data) + bs.broadcastmessage(_getTransText("Something_is_added") % + newReply, color=(0, 1, 0)) + bui.getsound("dingSmallHigh").play() + except: + babase.print_exception() + elif choice == "removeQuickReply": + Quickreply = self._get_quick_responds() + PopupMenuWindow(position=self._text_field.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=Quickreply, + choices_display=_creat_Lstr_list(Quickreply), + current_choice=Quickreply[0], + delegate=self) + self._popup_type = "removeQuickReplySelect" + elif choice in ("hostInfo_Debug",) and isinstance(bs.get_connection_to_host_info_2(), dict): + if bs.get_connection_to_host_info_2() != None: + # print(_babase.get_connection_to_host_info(),type(_babase.get_connection_to_host_info())) + + ChoiceDis = list(bs.get_connection_to_host_info_2().keys()) + NewChoices = ["bs.broadcastmessage(str(bs.get_connection_to_host_info_2().get('%s')))" % ( + (str(i)).replace("'", r"'").replace('"', r'\\"')) for i in ChoiceDis] + PopupMenuWindow(position=popup_window.root_widget.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=NewChoices, + choices_display=_creat_Lstr_list(ChoiceDis), + current_choice=NewChoices[0], + delegate=self) + + self._popup_type = "Custom_Exec_Choice" + else: + bui.getsound("error").play() + bs.broadcastmessage( + babase.Lstr(resource="getTicketsWindow.unavailableText"), color=(1, 0, 0)) + elif choice == "translator": + chats = _babase._getChatMessages() + if len(chats) > 0: + choices = [(item) for item in chats[::-1]] + PopupMenuWindow(position=popup_window.root_widget.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=choices, + choices_display=_creat_Lstr_list(choices), + current_choice=choices[0], + delegate=self) + self._popup_type = "translator_Press" + else: + bui.getsound("error").play() + bs.broadcastmessage( + babase.Lstr(resource="getTicketsWindow.unavailableText"), color=(1, 0, 0)) + elif choice == "resetGameRecord": + ConfirmWindow(text=_getTransText("Restart_Game_Record_Confirm"), + action=self._reset_game_record, cancel_button=True, cancel_is_selected=True, + color=self._bg_color, text_scale=1.0, + origin_widget=self.get_root_widget()) + elif self._popup_type == "translator_Press": + pass + + elif self._popup_type == "customAction_partyMemberPress": + + try: + keyReplaceValue = (r"{$PlayerNameFull}", r"{$PlayerName}", r"{$PlayerID}", + r"{$AccountInfo}", r"{$AllPlayerName}", r"{$AllPlayerNameFull}") + pos = None + curKeyWord = None + for keyWord in keyReplaceValue: + CurPos = choice.find(keyWord) + if CurPos != -1 and (pos is None or CurPos < pos): + pos = CurPos + curKeyWord = keyWord + if isinstance(pos, int) and isinstance(curKeyWord, str): + if curKeyWord in (r"{$PlayerNameFull}", r"{$PlayerName}", r"{$AllPlayerName}", r"{$AllPlayerNameFull}"): + # if choice.count(curKeyWord) != 0: + playerName = self._getObjectByID( + curKeyWord.replace("{$", "").replace("}", "")) + if isinstance(playerName, (list, tuple)): + ChoiceDis = [] + NewChoices = [] + for i in playerName: + ChoiceDis.append(i) + NewChoices.append(choice.replace( + curKeyWord, (i.replace("'", r"'").replace('"', r'\\"')), 1)) + p = PopupMenuWindow(position=popup_window.root_widget.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=NewChoices, + choices_display=_creat_Lstr_list(ChoiceDis), + current_choice=NewChoices[0], + delegate=self) + self._popup_type = "customAction_partyMemberPress" + elif isinstance(playerName, str): + self.popup_menu_selected_choice(popup_window, choice.replace( + curKeyWord, (playerName.replace("'", r"'").replace('"', r'\\"')), 1)) + else: + bs.broadcastmessage(_getTransText("No_valid_player_found"), (1, 0, 0)) + bui.getsound("error").play() + elif curKeyWord in (r"{$PlayerID}",) != 0: + playerID = self._getObjectByID("PlayerID") + playerName = self._getObjectByID("PlayerName") + # print(playerID,playerName) + if isinstance(playerID, (list, tuple)) and isinstance(playerName, (list, tuple)) and len(playerName) == len(playerID): + ChoiceDis = [] + NewChoices = [] + for i1, i2 in playerName, playerID: + ChoiceDis.append(i1) + NewChoices.append(choice.replace(r"{$PlayerID}", str( + i2).replace("'", r"'").replace('"', r'\\"')), 1) + p = PopupMenuWindow(position=popup_window.root_widget.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=NewChoices, + choices_display=_creat_Lstr_list(ChoiceDis), + current_choice=NewChoices[0], + delegate=self) + self._popup_type = "customAction_partyMemberPress" + elif isinstance(playerID, int): + self.popup_menu_selected_choice(popup_window, choice.replace( + r"{$PlayerID}", str(playerID).replace("'", r"'").replace('"', r'\\"'))) + else: + bs.broadcastmessage(_getTransText( + "No_valid_player_id_found"), (1, 0, 0)) + bui.getsound("error").play() + elif curKeyWord in (r"{$AccountInfo}",) != 0: + self.popup_menu_selected_choice(popup_window, choice.replace( + r"{$AccountInfo}", (str(self._getObjectByID("roster"))).replace("'", r"'").replace('"', r'\\"'), 1)) + else: + exec(choice) + except Exception as e: + bs.broadcastmessage(repr(e), (1, 0, 0)) + elif self._popup_type == "QuickMessageSelect": + # bui.textwidget(edit=self._text_field,text=self._get_quick_responds()[index]) + self._edit_text_msg_box(choice, "add") + elif self._popup_type == "removeQuickReplySelect": + data = self._get_quick_responds() + if len(data) > 0 and choice in data: + data.remove(choice) + self._write_quick_responds(data) + bs.broadcastmessage(_getTransText("Something_is_removed") % choice, (1, 0, 0)) + bui.getsound("shieldDown").play() + else: + bs.broadcastmessage(babase.Lstr(resource="errorText"), (1, 0, 0)) + bui.getsound("error").play() + elif choice.startswith("custom_Exec_Choice_") or self._popup_type == "Custom_Exec_Choice": + exec(choice[len("custom_Exec_Choice_"):] + if choice.startswith("custom_Exec_Choice_") else choice) + else: + print("unhandled popup type: "+str(self._popup_type)) + + +def fetchAccountInfo(account, loading_widget): + pbid = "" + account_data = [] + servers = [] + try: + filePath = os.path.join(RecordFilesDir, "players.json") + fdata = {} + if os.path.isfile(filePath): + f = open(filePath, "r") + fdata = json.load(f) + if account in fdata: + servers = fdata[account] + url = f'https://{BCSSERVER}/player?key={base64.b64encode(account.encode("utf-8")).decode("utf-8")}&base64=true' + req = urllib.request.Request(url, headers={ + "User-Agent": f'BS{_babase.env().get("build_number", 0)}', "Accept-Language": "en-US,en;q=0.9", }) + data = urllib.request.urlopen(req) + account_data = json.loads(data.read().decode('utf-8'))[0] + pbid = account_data["pbid"] + + except Exception as e: + print(e) + pass + # _babase.pushcall(Call(updateAccountWindow,loading_widget,accounts[0]),from_other_thread=True) + _babase.pushcall(Call(CustomAccountViewerWindow, pbid, account_data, + servers, loading_widget), from_other_thread=True) + + +class CustomAccountViewerWindow(viewer.AccountViewerWindow): + def __init__(self, account_id, custom_data, servers, loading_widget): + super().__init__(account_id) + try: + loading_widget._cancel() + except: + pass + self.custom_data = custom_data + self.pb_id = account_id + self.servers = servers + + def _copy_pb(self): + babase.clipboard_set_text(self.pb_id) + bs.broadcastmessage(babase.Lstr(resource='gatherWindow.copyCodeConfirmText')) + + def _on_query_response(self, data): + + if data is None: + bui.textwidget(edit=self._loading_text, text="") + bui.textwidget(parent=self._scrollwidget, + size=(0, 0), + position=(170, 200), + flatness=1.0, + h_align='center', + v_align='center', + scale=0.5, + color=bui.app.ui_v1.infotextcolor, + text="Mutual servers", + maxwidth=300) + v = 200-21 + for server in self.servers: + bui.textwidget(parent=self._scrollwidget, + size=(0, 0), + position=(170, v), + h_align='center', + v_align='center', + scale=0.55, + text=server, + maxwidth=300) + v -= 23 + else: + for account in self.custom_data["accounts"]: + if account not in data["accountDisplayStrings"]: + data["accountDisplayStrings"].append(account) + try: + self._loading_text.delete() + trophystr = '' + try: + trophystr = data['trophies'] + num = 10 + chunks = [ + trophystr[i:i + num] + for i in range(0, len(trophystr), num) + ] + trophystr = ('\n\n'.join(chunks)) + if trophystr == '': + trophystr = '-' + except Exception: + babase.print_exception('Error displaying trophies.') + account_name_spacing = 15 + tscale = 0.65 + ts_height = _babase.get_string_height(trophystr, + suppress_warning=True) + sub_width = self._width - 80 + sub_height = 500 + ts_height * tscale + \ + account_name_spacing * len(data['accountDisplayStrings']) + self._subcontainer = bui.containerwidget( + parent=self._scrollwidget, + size=(sub_width, sub_height), + background=False) + v = sub_height - 20 + + title_scale = 0.37 + center = 0.3 + maxwidth_scale = 0.45 + showing_character = False + if data['profileDisplayString'] is not None: + tint_color = (1, 1, 1) + try: + if data['profile'] is not None: + profile = data['profile'] + character = babase.app.spaz_appearances.get( + profile['character'], None) + if character is not None: + tint_color = (profile['color'] if 'color' + in profile else (1, 1, 1)) + tint2_color = (profile['highlight'] + if 'highlight' in profile else + (1, 1, 1)) + icon_tex = character.icon_texture + tint_tex = character.icon_mask_texture + mask_texture = bui.gettexture( + 'characterIconMask') + bui.imagewidget( + parent=self._subcontainer, + position=(sub_width * center - 40, v - 80), + size=(80, 80), + color=(1, 1, 1), + mask_texture=mask_texture, + texture=bui.gettexture(icon_tex), + tint_texture=bui.gettexture(tint_tex), + tint_color=tint_color, + tint2_color=tint2_color) + v -= 95 + except Exception: + babase.print_exception('Error displaying character.') + bui.textwidget( + parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.9, + color=babase.safecolor(tint_color, 0.7), + shadow=1.0, + text=babase.Lstr(value=data['profileDisplayString']), + maxwidth=sub_width * maxwidth_scale * 0.75) + showing_character = True + v -= 33 + + center = 0.75 if showing_character else 0.5 + maxwidth_scale = 0.45 if showing_character else 0.9 + + v = sub_height - 20 + if len(data['accountDisplayStrings']) <= 1: + account_title = babase.Lstr( + resource='settingsWindow.accountText') + else: + account_title = babase.Lstr( + resource='accountSettingsWindow.accountsText', + fallback_resource='settingsWindow.accountText') + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + flatness=1.0, + h_align='center', + v_align='center', + scale=title_scale, + color=bui.app.ui_v1.infotextcolor, + text=account_title, + maxwidth=sub_width * maxwidth_scale) + draw_small = (showing_character + or len(data['accountDisplayStrings']) > 1) + v -= 14 if draw_small else 20 + for account_string in data['accountDisplayStrings']: + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.55 if draw_small else 0.8, + text=account_string, + maxwidth=sub_width * maxwidth_scale) + v -= account_name_spacing + + v += account_name_spacing + v -= 25 if showing_character else 29 + # ======================================================================= + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + flatness=1.0, + h_align='center', + v_align='center', + scale=title_scale, + color=bui.app.ui_v1.infotextcolor, + text=str(self.pb_id), + maxwidth=sub_width * maxwidth_scale) + self._copy_btn = bui.buttonwidget( + parent=self._subcontainer, + position=(sub_width * center - 120, v - 9), + size=(60, 30), + scale=0.5, + label='copy', + color=(0.6, 0.5, 0.6), + on_activate_call=self._copy_pb, + autoselect=True) + + v -= 24 + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + flatness=1.0, + h_align='center', + v_align='center', + scale=title_scale, + color=bui.app.ui_v1.infotextcolor, + text="Name", + maxwidth=sub_width * maxwidth_scale) + v -= 26 + for name in self.custom_data["names"]: + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.51, + text=name, + maxwidth=sub_width * maxwidth_scale) + v -= 13 + v -= 8 + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + flatness=1.0, + h_align='center', + v_align='center', + scale=title_scale, + color=bui.app.ui_v1.infotextcolor, + text="Created On", + maxwidth=sub_width * maxwidth_scale) + v -= 19 + d = self.custom_data["createdOn"] + + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.55, + text=d[:d.index("T")], + maxwidth=sub_width * maxwidth_scale) + v -= 29 + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + flatness=1.0, + h_align='center', + v_align='center', + scale=title_scale, + color=bui.app.ui_v1.infotextcolor, + text="Discord", + maxwidth=sub_width * maxwidth_scale) + v -= 19 + if len(self.custom_data["discord"]) > 0: + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.55, + text=self.custom_data["discord"][0]["username"] + + ","+self.custom_data["discord"][0]["id"], + maxwidth=sub_width * maxwidth_scale) + v -= 26 + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + flatness=1.0, + h_align='center', + v_align='center', + scale=title_scale, + color=bui.app.ui_v1.infotextcolor, + text="Mutual servers", + maxwidth=sub_width * maxwidth_scale) + v = -19 + v = 270 + for server in self.servers: + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.55, + text=server, + maxwidth=sub_width * maxwidth_scale) + v -= 13 + + v -= 16 + # ================================================================== + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + flatness=1.0, + h_align='center', + v_align='center', + scale=title_scale, + color=bui.app.ui_v1.infotextcolor, + text=babase.Lstr(resource='rankText'), + maxwidth=sub_width * maxwidth_scale) + v -= 14 + if data['rank'] is None: + rank_str = '-' + suffix_offset = None + else: + str_raw = babase.Lstr( + resource='league.rankInLeagueText').evaluate() + # FIXME: Would be nice to not have to eval this. + rank_str = babase.Lstr( + resource='league.rankInLeagueText', + subs=[('${RANK}', str(data['rank'][2])), + ('${NAME}', + babase.Lstr(translate=('leagueNames', + data['rank'][0]))), + ('${SUFFIX}', '')]).evaluate() + rank_str_width = min( + sub_width * maxwidth_scale, + _babase.get_string_width(rank_str, suppress_warning=True) * + 0.55) + + # Only tack our suffix on if its at the end and only for + # non-diamond leagues. + if (str_raw.endswith('${SUFFIX}') + and data['rank'][0] != 'Diamond'): + suffix_offset = rank_str_width * 0.5 + 2 + else: + suffix_offset = None + + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.55, + text=rank_str, + maxwidth=sub_width * maxwidth_scale) + if suffix_offset is not None: + assert data['rank'] is not None + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center + suffix_offset, + v + 3), + h_align='left', + v_align='center', + scale=0.29, + flatness=1.0, + text='[' + str(data['rank'][1]) + ']') + v -= 14 + + str_raw = babase.Lstr( + resource='league.rankInLeagueText').evaluate() + old_offs = -50 + prev_ranks_shown = 0 + for prev_rank in data['prevRanks']: + rank_str = babase.Lstr( + value='${S}: ${I}', + subs=[ + ('${S}', + babase.Lstr(resource='league.seasonText', + subs=[('${NUMBER}', str(prev_rank[0]))])), + ('${I}', + babase.Lstr(resource='league.rankInLeagueText', + subs=[('${RANK}', str(prev_rank[3])), + ('${NAME}', + babase.Lstr(translate=('leagueNames', + prev_rank[1]))), + ('${SUFFIX}', '')])) + ]).evaluate() + rank_str_width = min( + sub_width * maxwidth_scale, + _babase.get_string_width(rank_str, suppress_warning=True) * + 0.3) + + # Only tack our suffix on if its at the end and only for + # non-diamond leagues. + if (str_raw.endswith('${SUFFIX}') + and prev_rank[1] != 'Diamond'): + suffix_offset = rank_str_width + 2 + else: + suffix_offset = None + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center + old_offs, v), + h_align='left', + v_align='center', + scale=0.3, + text=rank_str, + flatness=1.0, + maxwidth=sub_width * maxwidth_scale) + if suffix_offset is not None: + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center + old_offs + + suffix_offset, v + 1), + h_align='left', + v_align='center', + scale=0.20, + flatness=1.0, + text='[' + str(prev_rank[2]) + ']') + prev_ranks_shown += 1 + v -= 10 + + v -= 13 + + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + flatness=1.0, + h_align='center', + v_align='center', + scale=title_scale, + color=bui.app.ui_v1.infotextcolor, + text=babase.Lstr(resource='achievementsText'), + maxwidth=sub_width * maxwidth_scale) + v -= 14 + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.55, + text=str(data['achievementsCompleted']) + ' / ' + + str(len(bui.app.classic.ach.achievements)), + maxwidth=sub_width * maxwidth_scale) + v -= 25 + + if prev_ranks_shown == 0 and showing_character: + v -= 20 + elif prev_ranks_shown == 1 and showing_character: + v -= 10 + + center = 0.5 + maxwidth_scale = 0.9 + + bui.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=title_scale, + color=bui.app.ui_v1.infotextcolor, + flatness=1.0, + text=babase.Lstr(resource='trophiesThisSeasonText', + fallback_resource='trophiesText'), + maxwidth=sub_width * maxwidth_scale) + v -= 19 + bui.textwidget(parent=self._subcontainer, + size=(0, ts_height), + position=(sub_width * 0.5, + v - ts_height * tscale), + h_align='center', + v_align='top', + corner_scale=tscale, + text=trophystr) + + except Exception: + babase.print_exception('Error displaying account info.') + +# ba_meta export babase.Plugin + + +class bySmoothy(babase.Plugin): + def __init__(self): + bs.connect_to_party = newconnect_to_party + bascenev1lib_party.PartyWindow = ModifiedPartyWindow diff --git a/plugins/utilities/allow_invisible_models.py b/plugins/utilities/allow_invisible_models.py new file mode 100644 index 000000000..ec8355feb --- /dev/null +++ b/plugins/utilities/allow_invisible_models.py @@ -0,0 +1,17 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 9 +import babase +import bascenev1 as bs + +original_getmesh = bs.getmesh + + +def get_mesh_gracefully(mesh): + if mesh is not None: + return original_getmesh(mesh) + + +# ba_meta export plugin +class Main(babase.Plugin): + def on_app_running(self): + bs.getmesh = get_mesh_gracefully diff --git a/plugins/utilities/arabic_keyboard.py b/plugins/utilities/arabic_keyboard.py new file mode 100644 index 000000000..bb2ce9cb5 --- /dev/null +++ b/plugins/utilities/arabic_keyboard.py @@ -0,0 +1,32 @@ +# ba_meta require api 9 + +import bauiv1 + +# Full Arabic characters (real Arabic keyboard layout simulation) +arabic_chars = [ + list('ضصثقفغعهخحج'), # Row 1: QWERTY top + list('شسيبلاتنمكط'), # Row 2: QWERTY middle + list('ئءؤرلاىةوزظ'), # Row 3: QWERTY bottom (with 'لا' and more) +] + +# Fill each row to exactly 10 characters +for row in arabic_chars: + while len(row) < 10: + row.append('‎') # Invisible char + +# Arabic numerals and essential symbols +arabic_nums = list('١٢٣٤٥٦٧٨٩٠') + list('؟،؛ـ“”أإآًٍُِّْ')[:16] +while len(arabic_nums) < 26: + arabic_nums.append('‎') + +# ba_meta export bauiv1.Keyboard + + +class ArabicKeyboard(bauiv1.Keyboard): + """Arabic Keyboard by \ue048Freaku""" + name = 'Arabic Keyboard by yANES' + chars = arabic_chars + nums = arabic_nums + pages = { + 'symbols': tuple('!@#$%^&*()-_=+[{]}\\|;:\'",<.>/?')[:26] + } diff --git a/plugins/utilities/auto_stunt.py b/plugins/utilities/auto_stunt.py new file mode 100644 index 000000000..1f91667bd --- /dev/null +++ b/plugins/utilities/auto_stunt.py @@ -0,0 +1,563 @@ +# ba_meta require api 9 +# AutoStunt mod by - Mr.Smoothy x Rikko +# https://discord.gg/ucyaesh +# https://bombsquad-community.web.app/home +# Dont modify redistribute this plugin , if want to use features of this plugin in your mod write logic in seprate file +# and import this as module. +# If want to contribute in this original module, raise PR on github https://github.com/bombsquad-community/plugin-manager + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import bascenev1lib +from bascenev1lib.actor.image import Image +from bascenev1lib.actor import spaz +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.game.elimination import EliminationGame +import math +import json +import os + +from typing import Optional + +CONTROLS_CENTER = (0, 0) +CONTROLS_SCALE = 1 + +BASE_STUNTS_DIRECTORY = os.path.join(_babase.env()["python_directory_user"], "CustomStunts") +PLAYERS_STUNT_INFO = {} + +STUNT_CACHE = {} +original_on_begin = bs._activity.Activity.on_begin +original_chatmessage = bs.chatmessage + + +class ControlsUI: + + def on_jump_press(activity): + activity._jump_image.node.color = list( + channel * 2 for channel in activity._jump_image.node.color[:3]) + [1] + + def on_jump_release(activity): + activity._jump_image.node.color = list( + channel * 0.5 for channel in activity._jump_image.node.color[:3]) + [1] + + def on_pickup_press(activity): + activity._pickup_image.node.color = list( + channel * 2 for channel in activity._pickup_image.node.color[:3]) + [1] + + def on_pickup_release(activity): + activity._pickup_image.node.color = list( + channel * 0.5 for channel in activity._pickup_image.node.color[:3]) + [1] + + def on_punch_press(activity): + activity._punch_image.node.color = list( + channel * 2 for channel in activity._punch_image.node.color[:3]) + [1] + + def on_punch_release(activity): + activity._punch_image.node.color = list( + channel * 0.5 for channel in activity._punch_image.node.color[:3]) + [1] + + def on_bomb_press(activity): + activity._bomb_image.node.color = list( + channel * 2 for channel in activity._bomb_image.node.color[:3]) + [1] + + def on_bomb_release(activity): + activity._bomb_image.node.color = list( + channel * 0.5 for channel in activity._bomb_image.node.color[:3]) + [1] + + def on_move_ud(activity, value): + activity.set_stick_image_position(activity.stick_image_position_x, value) + + def on_move_lr(activity, value): + activity.set_stick_image_position(value, activity.stick_image_position_y) + + def display(activity): + activity._jump_image.node.color = list(activity._jump_image.node.color[:3]) + [1] + activity._pickup_image.node.color = list(activity._pickup_image.node.color[:3]) + [1] + activity._punch_image.node.color = list(activity._punch_image.node.color[:3]) + [1] + activity._bomb_image.node.color = list(activity._bomb_image.node.color[:3]) + [1] + activity._stick_base_image.opacity = 1.0 + activity._stick_nub_image.opacity = 1.0 + + def hide(activity): + activity._jump_image.node.color = list(activity._jump_image.node.color[:3]) + [0] + activity._pickup_image.node.color = list(activity._pickup_image.node.color[:3]) + [0] + activity._punch_image.node.color = list(activity._punch_image.node.color[:3]) + [0] + activity._bomb_image.node.color = list(activity._bomb_image.node.color[:3]) + [0] + activity._stick_base_image.opacity = 0.0 + activity._stick_nub_image.opacity = 0.0 + + +CONTROLS_UI_MAP = { + "JUMP_PRESS": ControlsUI.on_jump_press, + "JUMP_RELEASE": ControlsUI.on_jump_release, + "PICKUP_PRESS": ControlsUI.on_pickup_press, + "PICKUP_RELEASE": ControlsUI.on_pickup_release, + "PUNCH_PRESS": ControlsUI.on_punch_press, + "PUNCH_RELEASE": ControlsUI.on_punch_release, + "BOMB_PRESS": ControlsUI.on_bomb_press, + "BOMB_RELEASE": ControlsUI.on_bomb_release, + "UP_DOWN": ControlsUI.on_move_ud, + "LEFT_RIGHT": ControlsUI.on_move_lr +} + + +class NewSpaz(bascenev1lib.actor.spaz.Spaz): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.move_map = { + "UP_DOWN": self.on_move_up_down, + "LEFT_RIGHT": self.on_move_left_right, + "HOLD_POSITION": self.on_hold_position_press, + "HOLD_RELEASE": self.on_hold_position_release, + "JUMP_PRESS": self.on_jump_press, + "JUMP_RELEASE": self.on_jump_release, + "PICKUP_PRESS": self.on_pickup_press, + "PICKUP_RELEASE": self.on_pickup_release, + "PUNCH_PRESS": self.on_punch_press, + "PUNCH_RELEASE": self.on_punch_release, + "BOMB_PRESS": self.on_bomb_press, + "BOMB_RELEASE": self.on_bomb_release, + "RUN": self.on_run, + } + + +class NewPlayerSpaz(bascenev1lib.actor.playerspaz.PlayerSpaz): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.move_map = { + "UP_DOWN": self.on_move_up_down, + "LEFT_RIGHT": self.on_move_left_right, + "HOLD_POSITION": self.on_hold_position_press, + "HOLD_RELEASE": self.on_hold_position_release, + "JUMP_PRESS": self.on_jump_press, + "JUMP_RELEASE": self.on_jump_release, + "PICKUP_PRESS": self.on_pickup_press, + "PICKUP_RELEASE": self.on_pickup_release, + "PUNCH_PRESS": self.on_punch_press, + "PUNCH_RELEASE": self.on_punch_release, + "BOMB_PRESS": self.on_bomb_press, + "BOMB_RELEASE": self.on_bomb_release, + "RUN": self.on_run, + } + self.mirror_spaz = [] + self.source_player.in_replay = False + self.source_player.mirror_mode = False + + def _handle_action(self, action, value: Optional[float] = None) -> None: + if self.source_player.sessionplayer in PLAYERS_STUNT_INFO: + PLAYERS_STUNT_INFO[self.source_player.sessionplayer].append({ + "time": bs.time() - self.source_player.recording_start_time, + "move": { + "action": action, + "value": value, + } + }) + elif self.source_player.in_replay: + ui_activation = CONTROLS_UI_MAP.get(action) + if ui_activation: + if action in ["UP_DOWN", "LEFT_RIGHT"]: + ui_activation(self.source_player.actor._activity(), value) + else: + ui_activation(self.source_player.actor._activity()) + elif self.source_player.mirror_mode: + for mspaz in self.mirror_spaz: + if mspaz and mspaz.node.exists(): + if action in ["UP_DOWN", "LEFT_RIGHT", "RUN"]: + mspaz.move_map[action](value) + else: + mspaz.move_map[action]() + + def on_move_up_down(self, value: float, *args, **kwargs) -> None: + self._handle_action("UP_DOWN", value) + super().on_move_up_down(value, *args, **kwargs) + + def on_move_left_right(self, value: float, *args, **kwargs) -> None: + self._handle_action("LEFT_RIGHT", value) + super().on_move_left_right(value, *args, **kwargs) + + def on_hold_position_press(self, *args, **kwargs) -> None: + self._handle_action("HOLD_POSITION") + super().on_hold_position_press(*args, **kwargs) + + def on_hold_position_release(self, *args, **kwargs) -> None: + self._handle_action("HOLD_RELEASE") + super().on_hold_position_release(*args, **kwargs) + + def on_jump_press(self, *args, **kwargs) -> None: + self._handle_action("JUMP_PRESS") + super().on_jump_press(*args, **kwargs) + + def on_jump_release(self, *args, **kwargs) -> None: + self._handle_action("JUMP_RELEASE") + super().on_jump_release(*args, **kwargs) + + def on_pickup_press(self, *args, **kwargs) -> None: + self._handle_action("PICKUP_PRESS") + super().on_pickup_press(*args, **kwargs) + + def on_pickup_release(self, *args, **kwargs) -> None: + self._handle_action("PICKUP_RELEASE") + super().on_pickup_release(*args, **kwargs) + + def on_punch_press(self, *args, **kwargs) -> None: + self._handle_action("PUNCH_PRESS") + super().on_punch_press(*args, **kwargs) + + def on_punch_release(self, *args, **kwargs) -> None: + self._handle_action("PUNCH_RELEASE") + super().on_punch_release(*args, **kwargs) + + def on_bomb_press(self, *args, **kwargs) -> None: + self._handle_action("BOMB_PRESS") + super().on_bomb_press(*args, **kwargs) + + def on_bomb_release(self, *args, **kwargs) -> None: + self._handle_action("BOMB_RELEASE") + super().on_bomb_release(*args, **kwargs) + + def on_run(self, value: float, *args, **kwargs) -> None: + self._handle_action("RUN", value) + super().on_run(value, *args, **kwargs) + + +def handle_player_replay_end(player): + player.in_replay = False + ControlsUI.hide(player.actor._activity()) + + +def get_player_from_client_id(client_id, activity=None): + activity = activity or bs.get_foreground_host_activity() + for player in activity.players: + if player.sessionplayer.inputdevice.client_id == client_id: + return player + raise bs.SessionPlayerNotFound() + + +def mirror(clieid): + player = get_player_from_client_id(clieid) + spawn_mirror_spaz(player) + + +def capture(player): + with player.actor._activity().context: + player.recording_start_time = bs.time() + PLAYERS_STUNT_INFO[player.sessionplayer] = [] + + +def save(player, stunt_name): + stunt_path = f"{os.path.join(BASE_STUNTS_DIRECTORY, stunt_name)}.json" + os.makedirs(BASE_STUNTS_DIRECTORY, exist_ok=True) + with open(stunt_path, "w") as fout: + json.dump(PLAYERS_STUNT_INFO[player.sessionplayer], fout, indent=2) + del PLAYERS_STUNT_INFO[player.sessionplayer] + + +def replay(player, stunt_name): + stunt_path = f"{os.path.join(BASE_STUNTS_DIRECTORY, stunt_name)}.json" + if stunt_name in STUNT_CACHE: + stunt = STUNT_CACHE[stunt_name] + else: + try: + with open(stunt_path, "r") as fin: + stunt = json.load(fin) + STUNT_CACHE[stunt_name] = stunt + except: + bui.screenmessage(f"{stunt_name} doesn't exists") + return + player.in_replay = True + with player.actor._activity().context: + ControlsUI.display(player.actor._activity()) + for move in stunt: + value = move["move"]["value"] + if value is None: + bs.timer( + move["time"], + babase.Call(player.actor.move_map[move["move"]["action"]]) + ) + else: + bs.timer( + move["time"], + babase.Call(player.actor.move_map[move["move"] + ["action"]], move["move"]["value"]) + ) + last_move_time = move["time"] + time_to_hide_controls = last_move_time + 1 + bs.timer(time_to_hide_controls, babase.Call(handle_player_replay_end, player)) + + +def spawn_mirror_spaz(player): + player.mirror_mode = True + with player.actor._activity().context: + bot = spaz.Spaz( + color=player.color, + highlight=player.highlight, + character=player.character + ).autoretain() + bot.handlemessage(bs.StandMessage( + (player.actor.node.position[0], player.actor.node.position[1], player.actor.node.position[2]+1), 93)) + bot.node.name = player.actor.node.name + bot.node.name_color = player.actor.node.name_color + player.actor.mirror_spaz.append(bot) + + +def ghost(player, stunt_name): + stunt_path = f"{os.path.join(BASE_STUNTS_DIRECTORY, stunt_name)}.json" + if stunt_name in STUNT_CACHE: + stunt = STUNT_CACHE[stunt_name] + else: + try: + with open(stunt_path, "r") as fin: + stunt = json.load(fin) + STUNT_CACHE[stunt_name] = stunt + except: + bui.screenmessage(f"{stunt_name} doesn't exists") + return + player.in_replay = True + + with player.actor._activity().context: + bot = spaz.Spaz(color=(1, 0, 0), character="Spaz").autoretain() + bot.handlemessage(bs.StandMessage(player.actor.node.position, 93)) + give_ghost_power(bot) + ControlsUI.display(bot._activity()) + for move in stunt: + value = move["move"]["value"] + if value is None: + bs.timer( + move["time"], + babase.Call(bot.move_map[move["move"]["action"]]) + ) + ui_activation = CONTROLS_UI_MAP.get(move["move"]["action"]) + if ui_activation: + bs.timer( + move["time"], + babase.Call(ui_activation, player.actor._activity()) + ) + else: + bs.timer( + move["time"], + babase.Call(bot.move_map[move["move"]["action"]], move["move"]["value"]) + ) + ui_activation = CONTROLS_UI_MAP.get(move["move"]["action"]) + + if ui_activation: + bs.timer( + move["time"], + babase.Call(ui_activation, player.actor._activity(), move["move"]["value"]) + ) + last_move_time = move["time"] + time_to_hide_controls = last_move_time + 1 + bs.timer(time_to_hide_controls, babase.Call(handle_player_replay_end, player)) + bs.timer(time_to_hide_controls, babase.Call(bot.node.delete)) + + +def give_ghost_power(spaz): + spaz.node.invincible = True + shared = SharedObjects.get() + factory = SpazFactory.get() + ghost = bs.Material() + # smoothy hecks + ghost.add_actions( + conditions=(('they_have_material', factory.spaz_material), 'or', + ('they_have_material', shared.player_material), 'or', + ('they_have_material', shared.attack_material), 'or', + ('they_have_material', shared.pickup_material), 'or', + ('they_have_material', PowerupBoxFactory.get().powerup_accept_material)), + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + )) + mats = list(spaz.node.materials) + roller = list(spaz.node.roller_materials) + ext = list(spaz.node.extras_material) + pick = list(spaz.node.pickup_materials) + punch = list(spaz.node.punch_materials) + + mats.append(ghost) + roller.append(ghost) + ext.append(ghost) + pick.append(ghost) + punch.append(ghost) + + spaz.node.materials = tuple(mats) + spaz.node.roller_materials = tuple(roller) + spaz.node.extras_material = tuple(ext) + spaz.node.pickup_materials = tuple(pick) + spaz.node.punch_materials = tuple(pick) + + +def new_chatmessage(msg): + #! Fix here to make it work with other mods modifying chat message + if not msg.startswith("*"): + return original_chatmessage(msg) + + stripped_msg = msg[1:] + msg_splits = stripped_msg.split(maxsplit=3) + command = msg_splits[0] + + client_id = -1 + try: + player = get_player_from_client_id(client_id) + except AttributeError: + bui.screenmessage("Start a game to use", color=(0, 1, 0)) + return + + if command == "start": + capture(player) + bs.chatmessage("Recording started for {}.".format( + player.getname(), + )) + return original_chatmessage(msg) + + stunt_name = " ".join(msg_splits[1:]) + + if command == "save": + if len(msg_splits) < 2: + bui.screenmessage("Enter name of stunt eg : *save bombjump") + return original_chatmessage(msg) + save(player, stunt_name) + bs.chatmessage('Recording "{}" by {} saved.'.format( + stunt_name, + player.getname(), + )) + elif command == "stunt": + if len(msg_splits) < 2: + bui.screenmessage("Enter name of stunt eg : *stunt bombjump") + return original_chatmessage(msg) + if player is not None and player.actor is not None: + replay(player, stunt_name) + bs.chatmessage('Replaying "{}" on {}.'.format( + stunt_name, + player.getname(), + )) + else: + bui.screenmessage("Player not found") + elif command == "learn": + if len(msg_splits) < 2: + bui.screenmessage("Enter name of stunt eg : *learn bombjump") + return original_chatmessage(msg) + if player is not None and player.actor is not None: + ghost(player, stunt_name) + bs.chatmessage('Replaying "{}" on {}.'.format( + stunt_name, + player.getname(), + )) + else: + bui.screenmessage("Player not found") + elif command == "mirror": + spawn_mirror_spaz(player) + return original_chatmessage(msg) + + +def set_stick_image_position(self, x: float, y: float) -> None: + + # Clamp this to a circle. + len_squared = x * x + y * y + if len_squared > 1.0: + length = math.sqrt(len_squared) + mult = 1.0 / length + x *= mult + y *= mult + + self.stick_image_position_x = x + self.stick_image_position_y = y + offs = 50.0 + assert self._scale is not None + p = [ + self._stick_nub_position[0] + x * offs * 0.6, + self._stick_nub_position[1] + y * offs * 0.6 + ] + c = list(self._stick_nub_image_color) + if abs(x) > 0.1 or abs(y) > 0.1: + c[0] *= 2.0 + c[1] *= 4.0 + c[2] *= 2.0 + assert self._stick_nub_image is not None + self._stick_nub_image.position = p + self._stick_nub_image.color = c + c = list(self._stick_base_image_color) + if abs(x) > 0.1 or abs(y) > 0.1: + c[0] *= 1.5 + c[1] *= 1.5 + c[2] *= 1.5 + assert self._stick_base_image is not None + self._stick_base_image.color = c + + +def on_begin(self, *args, **kwargs) -> None: + self._jump_image = Image( + bs.gettexture('buttonJump'), + position=(385, 160), + scale=(50, 50), + color=[0.1, 0.45, 0.1, 0] + ) + self._pickup_image = Image( + bs.gettexture('buttonPickUp'), + position=(385, 240), + scale=(50, 50), + color=[0, 0.35, 0, 0] + ) + self._punch_image = Image( + bs.gettexture('buttonPunch'), + position=(345, 200), + scale=(50, 50), + color=[0.45, 0.45, 0, 0] + ) + self._bomb_image = Image( + bs.gettexture('buttonBomb'), + position=(425, 200), + scale=(50, 50), + color=[0.45, 0.1, 0.1, 0] + ) + self.stick_image_position_x = self.stick_image_position_y = 0.0 + self._stick_base_position = p = (-328, 200) + self._stick_base_image_color = c2 = (0.25, 0.25, 0.25, 1.0) + self._stick_base_image = bs.newnode( + 'image', + attrs={ + 'texture': bs.gettexture('nub'), + 'absolute_scale': True, + 'vr_depth': -40, + 'position': p, + 'scale': (220.0*0.6, 220.0*0.6), + 'color': c2 + }) + self._stick_nub_position = p = (-328, 200) + self._stick_nub_image_color = c3 = (0.4, 0.4, 0.4, 1.0) + self._stick_nub_image = bs.newnode('image', + attrs={ + 'texture': bs.gettexture('nub'), + 'absolute_scale': True, + 'position': p, + 'scale': (110*0.6, 110*0.66), + 'color': c3 + }) + self._stick_base_image.opacity = 0.0 + self._stick_nub_image.opacity = 0.0 + return original_on_begin(self, *args, **kwargs) + +# ba_meta export babase.Plugin + + +class byHeySmoothy(babase.Plugin): + def on_app_running(self): + bs._activity.Activity.on_begin = on_begin + bs._activity.Activity.set_stick_image_position = set_stick_image_position + bs.chatmessage = new_chatmessage + bascenev1lib.actor.playerspaz.PlayerSpaz = NewPlayerSpaz + bascenev1lib.actor.spaz.Spaz = NewSpaz + + +# lets define a sample elimination game that can use super power of this plugin + +# ba_meta export bascenev1.GameActivity +class BroEliminaition(EliminationGame): + name = 'BroElimination' + description = 'Elimination Game with dual character control' + + def spawn_player(self, player) -> bs.Actor: + super().spawn_player(player) + spawn_mirror_spaz(player) diff --git a/plugins/utilities/autorun.py b/plugins/utilities/autorun.py new file mode 100644 index 000000000..737e0215d --- /dev/null +++ b/plugins/utilities/autorun.py @@ -0,0 +1,277 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) + +# ba_meta require api 9 + +""" + AutoRun by TheMikirog + Version 1 + + Run without holding any buttons. Made for beginners or players on mobile. + Keeps your character maneuverable. + Start running as usual to override. + + Heavily commented for easy modding learning! + + No Rights Reserved +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +# Let's import everything we need and nothing more. +import babase +import bauiv1 as bui +import bascenev1 as bs +import bascenev1lib +import math +from bascenev1lib.actor.spaz import Spaz + +if TYPE_CHECKING: + pass + +""" + This mod is much more "technical" than my other mods. + I highly recommend checking out the code of this mod once you have a good understanding of programming. + At the very least check out my other heavily commented mods like my Hot Potato gamemode. It's pretty dank! + Normally you shouldn't flood your scripts with comments like that. + I do it here to help people like you get the basic tools required to make your own mods similar to this one. + If you write your own code, only comment what can't be easily inferred from reading the code alone. + Consider this an interactive tutorial of sorts. + + Let's start with the goal of this mod; the conception. + If you play on mobile, the only way you get to run is if you press and hold any other action button like jump or punch. + Basically, all action buttons do two things at once unless a gamemode disables one of those actions. + Playing on a gamepad or keyboard gives you the luxury of a dedicated run button, which gives you much more control + over your movement. This basically forces mobile players that are running to: + - Punch and risk being open to attacks. + - Kill all your momentum by jumping. + - Using the bomb to run, but only after using that same button to throw an already held bomb. + - Using an inconvenient out of the way grab button to avoid all of that hassle. + It's a mess. Get a gamepad. + This mod exists as an alternative to those who can't play on a gamepad, but don't want + to be inconvenienced by running quirks if they JUST WANT TO PLAY. + + The naive implementation of this would be to just running all the time, but here's the catch: + Running makes turning less tight, which is the compromise for being really fast. + If you want to have tighter turns, you'd release the run button for a split second, turn and press it again. + Much easier and more convenient to do if you're on a gamepad. + The goal of this mod is to replicate this behavior and making it automatic. + My aim is to get the player moving as fast as possible without making it significantly harder to control. + This is supposed to help mobile players, not handicap them. + I can imagine the sweet relief of not being forced to babysit an action button just for running fast. + Actually it should help gamepad users too, since holding your trigger can be exhausting + or even impossible for those with physical disabilities. + + For your information, I started writing this mod THREE times. + Each time with the goal of trying out different ways of achieving my goals. + I used the code and failures of the previous scripts to make the next one better. + What you're seeing here is the final iteration; the finished product. + Don't expect your code to look like this the first time, especially if you're trying something ballsy. + You will fail, but don't be afraid to experiment. + Only through experimentation you can forge a failure into a success. +""" + +# ba_meta export babase.Plugin + + +class AutoRun(babase.Plugin): + # During my research and prototyping I figured I'd have to do some linear algebgra. + # I didn't want to use libraries, since this is supposed to be a standalone mod. + # Because of this I made certain functions from scratch that are easily accessible. + # If you are curious over the details, look these up on the Internet. + # I'll only briefly cover their purpose in the context of the mod. + + # Here's the dot product function. + # To keep it short, it returns the difference in angle between two vectors. + # We're gonna use that knowledge to check how tight our turn is. + # I'll touch on that later. + def dot(vector_a, vector_b): + return vector_a[0] * vector_b[0] + vector_a[1] * vector_b[1] + + # This clamping function will make sure a certain value won't go above or below a certain threshold. + # self.node.run attribute expects a value between 0-1, so this is one way of enforcing this. + def clamp(num, min_value, max_value): + num = max(min(num, max_value), min_value) + return num + + # A vector can be of any length, but we need them to be of length 1. + # This vector normalization function changes the magnitude of a vector without changing its direction. + def normalize(vector): + length = math.hypot(vector[0], vector[1]) # Pythagoras says hi + # Sometimes we'll get a [0,0] vector and dividing by 0 is iffy. + # Let's leave the vector unchanged if that's the case. + if length > 0: + return [vector[0] / length, vector[1] / length] + else: + return vector + + # We use a decorator to add extra code to existing code, increasing mod compatibility. + # We're gonna use decorators ALOT in this mod. + # Here I'm defining a new spaz init function that'll be replaced. + def new_init(func): + def wrapper(*args, **kwargs): + # Here's where we execute the original game's code, so it's not lost. + # We want to add our code at the end of the existing code, so our code goes under that. + func(*args, **kwargs) + + # We define some variables that we need to keep track of. + # For future reference, if you see args[0] anywhere, that is "self" in the original function. + args[0].autorun_timer: bs.Timer | None = None + args[0].autorun_override = False + + # We wanna do our auto run calculations when the player moves their analog stick to make it responsive. + # However doing this ONLY tracks changes in analog stick position and some bugs come up because of that. + # For example moving via dpad on a gamepad can sometimes not execute the run at all. + # To keep the behavior predictable, we also want to update our auto run functionality with a periodic timer. + # We could ignore the update on analog stick movement, but then it feels terrible to play. We need both. + # Update on analog movement for responsive controls, timer to foolproof everything else. + + # To make our timer, we want to have access to our function responsible for doing the auto run logic. + # The issue is that timers only work when a function is created within the context of the game. + # Timer throws a tantrum if it references the run_update function, but NOT if that function is an intermediary. + def spaz_autorun_update(): + AutoRun.run_update(args[0]) + + # We don't want this logic to be ran on bots, only players. + # Check if we have a player assigned to that spaz. If we do, let's make our timer. + if args[0].source_player: + # And here's our timer. + # It loops indefinitely thanks to the 'repeat' argument that is set to True. + # Notice how it's the capital T Timer instead of the small letter. + # That's important, because big T returns a timer object we can manipulate. + # We need it assigned to a variable, because we have to delete it once it stops being relevant. + args[0].autorun_timer = bs.Timer(0.1, spaz_autorun_update, repeat=True) + + return wrapper + + # Let's replace the original function with our modified version. + bascenev1lib.actor.spaz.Spaz.__init__ = new_init( + bascenev1lib.actor.spaz.Spaz.__init__ + ) + + # This is the bulk of our mod. Our run_update function. + # The goal here is to change the self.node.run attribute of our character. + # This attribute handles running behavior based on how far we pushed the running trigger. + # 0 means not running and 1 means run trigger fully pressed. + # On mobile it's always 0 and 1, but on gamepad you can have values between them + # For example you can do a jog instead of a sprint. + # We activate this function periodically via a timer and every time the player moves their analog stick. + # The idea is to make it 1 when the player is running forward and make it 0 + # when the player makes the tightest turn possible. + # We also want to account for how far the analog stick is pushed. + def run_update(self) -> None: + # Let's not run this code if our character does not exist or the player decides to run "manually". + if not self.node or self.autorun_override: + return + + # Let's read our player's analog stick. + # Notice how the vertical direction is inverted (there's a minus in front of the variable). + # We want the directions to corespond to the game world. + vertical = -self.node.move_up_down + horizontal = self.node.move_left_right + movement_vector = [horizontal, vertical] + + # Get our character's facing direction + facing_direction = ( + self.node.position[0] - self.node.position_forward[0], + self.node.position[2] - self.node.position_forward[2], + ) + # We want our character's facing direction to be a normalized vector (magnitude of 1). + facing_direction = AutoRun.normalize(facing_direction) + + # We don't want to run our code if the player has their analog stick in a neutral position. + if movement_vector == [0.0, 0.0]: + return + + # Get the difference between our current facing direction and where we plan on moving towards. + # Check the dot function higher up in the script for details. + dot = AutoRun.dot(facing_direction, AutoRun.normalize(movement_vector)) + if dot > 0.0: + # Our dot value ranges from -1 to 1. + # We want it from 0 to 1. + # 0 being 180 degree turn, 1 being running exactly straight. + dot = (dot + 1) / 2 + + # Let's read how far our player pushed his stick. 1 being full tilt, 0 being neutral. + # Heres our homie Pythagoras once again + run_power = math.hypot(movement_vector[0], movement_vector[1]) + + # I noticed the player starts running too fast if the stick is pushed half-way. + # I changed the linear scale to be exponential. + # easings.net is a great website that shows you different ways of converting a linear curve to some other kind. + # Here I used the EaseInQuad easing, which is just raising the value to the power of 2. + # This should make half-way pushes less severe. + run_power = pow(run_power, 2) + + # Just in case let's clamp our value from 0 to 1. + run_power = AutoRun.clamp(run_power, 0.0, 1.0) + + # Here we combine our dot result with how far we pushed our stick to get the final running value. + # Clamping from 0 to 1 for good measure. + self.node.run = AutoRun.clamp(run_power * dot, 0.0, 1.0) + + # This function is called every time we want to run or touch a running trigger. + # We have our auto run stuff, but we also want for our mod to play nice with the current running behavior. + # We also want this to work with my Quickturn mod. + def new_onrun(func): + def wrapper(*args, **kwargs): + # When we hold an action button or press our running trigger at any point, our mod should stop interfering. + # This won't work if your gamepad has borked triggers though. + args[0].autorun_override = args[1] + # Here's our original unchanged function + func(*args, **kwargs) + + return wrapper + + # We replace the character running function with our modified version. + bascenev1lib.actor.spaz.Spaz.on_run = new_onrun(bascenev1lib.actor.spaz.Spaz.on_run) + + # There's two function that are called when our player pushes the analog stick - two for each axis. + # Here's for the vertical axis. + def new_updown(func): + def wrapper(*args, **kwargs): + # Original function + func(*args, **kwargs) + # If we're not holding the run button and we're a player, run our auto run behavior. + if not args[0].autorun_override and args[0].source_player: + AutoRun.run_update(args[0]) + + return wrapper + + # You get the idea. + bascenev1lib.actor.spaz.Spaz.on_move_up_down = new_updown( + bascenev1lib.actor.spaz.Spaz.on_move_up_down + ) + + # Let's do the same for our horizontal axis. + # Second verse same as the first. + def new_leftright(func): + def wrapper(*args, **kwargs): + func(*args, **kwargs) + if not args[0].autorun_override and args[0].source_player: + AutoRun.run_update(args[0]) + + return wrapper + + bascenev1lib.actor.spaz.Spaz.on_move_left_right = new_leftright( + bascenev1lib.actor.spaz.Spaz.on_move_left_right + ) + + # There's one downside to the looping timer - it runs constantly even if the player is dead. + # We don't want to waste computational power on something like that. + # Let's kill our timer when the player dies. + def new_handlemessage(func): + def wrapper(*args, **kwargs): + # Only react to the death message. + if isinstance(args[1], bs.DieMessage): + # Kill the timer. + args[0].autorun_timer = None + # Original function. + func(*args, **kwargs) + + return wrapper + + bascenev1lib.actor.spaz.Spaz.handlemessage = new_handlemessage( + bascenev1lib.actor.spaz.Spaz.handlemessage + ) diff --git a/plugins/utilities/bomb_radius_visualizer.py b/plugins/utilities/bomb_radius_visualizer.py new file mode 100644 index 000000000..298f91d2e --- /dev/null +++ b/plugins/utilities/bomb_radius_visualizer.py @@ -0,0 +1,96 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 9 + +""" + Bomb Radius Visualizer by TheMikirog + + With this cutting edge technology, you precisely know + how close to the bomb you can tread. Supports modified blast radius values! + + Heavily commented for easy modding learning! + + No Rights Reserved +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +# Let's import everything we need and nothing more. +import babase +import bauiv1 as bui +import bascenev1 as bs +import bascenev1lib +from bascenev1lib.actor.bomb import Bomb + +if TYPE_CHECKING: + pass + +# ba_meta export babase.Plugin + + +class BombRadiusVisualizer(babase.Plugin): + + # We use a decorator to add extra code to existing code, increasing mod compatibility. + # Here I'm defining a new bomb init function that'll be replaced. + def new_bomb_init(func): + # This function will return our wrapper function, which is going to take the original function's base arguments. + # Yes, in Python functions are objects that can be passed as arguments. It's bonkers. + # arg[0] is "self" in our original bomb init function. + # We're working kind of blindly here, so it's good to have the original function + # open in a second window for argument reference. + def wrapper(*args, **kwargs): + # Here's where we execute the original game's code, so it's not lost. + # We want to add our code at the end of the existing code, so our code goes under that. + func(*args, **kwargs) + + # Let's make a new node that's just a circle. It's the some one used in the Target Practice minigame. + # This is going to make a slightly opaque red circle, signifying damaging area. + # We aren't defining the size, because we're gonna animate it shortly after. + args[0].radius_visualizer = bs.newnode('locator', + # Remove itself when the bomb node dies. + owner=args[0].node, + attrs={ + 'shape': 'circle', + 'color': (1, 0, 0), + 'opacity': 0.05, + 'draw_beauty': False, + 'additive': False + }) + # Let's connect our circle to the bomb. + args[0].node.connectattr('position', args[0].radius_visualizer, 'position') + + # Let's do a fancy animation of that red circle growing into shape like a cartoon. + # We're gonna read our bomb's blast radius and use it to decide the size of our circle. + bs.animate_array(args[0].radius_visualizer, 'size', 1, { + 0.0: [0.0], + 0.2: [args[0].blast_radius * 2.2], + 0.25: [args[0].blast_radius * 2.0] + }) + + # Let's do a second circle, this time just the outline to where the damaging area ends. + args[0].radius_visualizer_circle = bs.newnode('locator', + # Remove itself when the bomb node dies. + owner=args[0].node, + attrs={ + 'shape': 'circleOutline', + # Here's that bomb's blast radius value again! + 'size': [args[0].blast_radius * 2.0], + 'color': (1, 1, 0), + 'draw_beauty': False, + 'additive': True + }) + # Attach the circle to the bomb. + args[0].node.connectattr('position', args[0].radius_visualizer_circle, 'position') + + # Let's animate that circle too, but this time let's do the opacity. + bs.animate( + args[0].radius_visualizer_circle, 'opacity', { + 0: 0.0, + 0.4: 0.1 + }) + return wrapper + + # Finally we """travel through the game files""" to replace the function we want with our own version. + # We transplant the old function's arguments into our version. + bascenev1lib.actor.bomb.Bomb.__init__ = new_bomb_init(bascenev1lib.actor.bomb.Bomb.__init__) diff --git a/plugins/utilities/bots_can_accept_powerups.py b/plugins/utilities/bots_can_accept_powerups.py new file mode 100644 index 000000000..303831c8c --- /dev/null +++ b/plugins/utilities/bots_can_accept_powerups.py @@ -0,0 +1,42 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 9 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazbot import SpazBot +from bascenev1lib.actor.powerupbox import PowerupBoxFactory + +if TYPE_CHECKING: + pass + + +# ba_meta export babase.Plugin +class BotsCanAcceptPowerupsPlugin(babase.Plugin): + def on_app_running(self) -> None: + SpazBot.oldinit = SpazBot.__init__ + + def __init__(self) -> None: + self.oldinit() + pam = PowerupBoxFactory.get().powerup_accept_material + materials = self.node.materials + materials = list(materials) + materials.append(pam) + materials = tuple(materials) + self.node.materials = materials + roller_materials = self.node.roller_materials + roller_materials = list(roller_materials) + roller_materials.append(pam) + roller_materials = tuple(roller_materials) + self.node.roller_materials = roller_materials + extras_material = self.node.extras_material + extras_material = list(extras_material) + extras_material.append(pam) + extras_material = tuple(extras_material) + self.node.extras_material = extras_material + SpazBot.__init__ = __init__ diff --git a/plugins/utilities/camera.py b/plugins/utilities/camera.py new file mode 100644 index 000000000..7ebe0218a --- /dev/null +++ b/plugins/utilities/camera.py @@ -0,0 +1,643 @@ +# Copyright 2025 - Solely by BrotherBoard +# Bug? Feedback? Telegram >> @GalaxyA14user + +""" +Camera v1.0 - Say cheese. + +Adds a button to pause menu. Camera is advanced +Camera allows you to change camera position and +target with a very easy graphical visualization +of how it would look like. +""" + +from _babase import ( + get_display_resolution as GDR, + clipboard_is_supported as CIS, + set_camera_position as SCP, + clipboard_set_text as COPY, + set_camera_manual as SSCM, + set_camera_target as SCT +) +from bascenev1 import ( + get_foreground_host_activity as ga, + OutOfBoundsMessage, + gettexture as gbt, + getsound as gbs, + timer as tick, + Material, + getmesh, + newnode, + animate, + emitfx +) +from bauiv1 import ( + get_special_widget as gsw, + containerwidget as cw, + screenmessage as push, + buttonwidget as bw, + imagewidget as iw, + textwidget as tw, + gettexture as gt, + apptimer as teck, + getsound as gs, + app as APP +) +from bauiv1lib.ingamemenu import InGameMenuWindow as igm +from babase import Plugin, InputType as IT +from math import sqrt, dist + + +class Camera: + __doc__ = 'A simple camera.' + __ins__ = None + __lst__ = None + __yes__ = False + + def __init__(s) -> None: + c = s.__class__ + if c.__yes__: + note('Stopped camera!', True) + c.__ins__.done(talk=False) + return + c.__ins__ = s + c.__yes__ = True + if c.__lst__: + SCM(False) + s.stage = 0 + p = (0, 1, 0) + s.tex = 'achievementCrossHair' + s.kids = [] + s.okay = [] + with ga().context: + s.M = Material() + s.M.add_actions( + conditions=(('they_are_older_than', 0)), + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False), + ('modify_part_collision', 'friction', 0), + ('modify_part_collision', 'stiffness', 0), + ('modify_part_collision', 'damping', 0) + ) + ) + n = newnode( + 'prop', + delegate=s, + attrs={ + 'mesh': getmesh('tnt'), + 'color_texture': gbt(s.tex), + 'body': 'crate', + 'reflection': 'soft', + 'density': 4.0, + 'reflection_scale': [1.5], + 'shadow_size': 0.6, + 'position': p, + 'gravity_scale': 0, + 'materials': [s.M], + 'is_area_of_interest': True + } + ) + tick(0.15, animate(n, 'mesh_scale', {0: 2, 0.1: 0.5}).delete) + gbs('powerup01').play(position=p) + s.step = 0.01 + s.node = n + s.wait = 0.001 + s.mode = 4 + s.llr = s.lud = 0.0 + s.overlay = Overlay() + LN({ + 'UP_DOWN': lambda a: s.manage(a), + 'LEFT_RIGHT': lambda a: s.manage(a, 1), + 'PICK_UP_PRESS': lambda: s.start(2), + 'JUMP_PRESS': lambda: s.start(0), + 'PICK_UP_RELEASE': lambda: s.stop(2), + 'JUMP_RELEASE': lambda: s.stop(0), + 'BOMB_PRESS': s.done, + 'BOMB_RELEASE': lambda: s.overlay.release(1), + 'PUNCH_PRESS': s.mark, + 'PUNCH_RELEASE': lambda: s.overlay.release(3), + }) + s.move() + """Write a tip""" + def tip(s, t, p, h='left', b=True): + n = newnode( + 'text', + attrs={ + 'in_world': True, + 'scale': 0.01, + 'flatness': 1, + 'color': (1, 1, 1), + 'shadow': 1.0, + 'position': p, + 'text': t, + 'h_align': h + } + ) + if b: + s.kids.append(n) + return n + """Create a dot""" + def dot(s, p, b=True, tex='black'): + n = newnode( + 'prop', + delegate=s, + attrs={ + 'mesh': getmesh('tnt'), + 'color_texture': gbt(tex), + 'body': 'crate', + 'mesh_scale': 0.1, + 'position': p, + 'gravity_scale': 0, + 'materials': [s.M], + } + ) + if b: + s.kids.append(n) + return n + """Draw a line""" + def line(s, p1, p2, i=2, tex='black'): + x1, y1, z1 = p1 + x2, y2, z2 = p2 + n = dist(p1, p2)*i + for i in range(1, int(n+1)): + t = i/n + x = x1+t*(x2-x1) + y = y1+t*(y2-y1) + z = z1+t*(z2-z1) + s.kids.append(s.dot((x, y, z), tex=tex)) + """Mark""" + def mark(s): + if not s.stage: + s.stage = 1 + p = s.getpos() + s.p1 = (p[0]-0.01, p[1], p[2]) + s.okay.append(s.dot(s.p1, b=False)) + s.okay.append( + s.tip(f'Camera position\n{tuple([round(i, 2) for i in s.p1])}', s.p1, b=False)) + else: + [i.delete() for i in s.kids] + s.kids.clear() + p2 = s.p2 = s.getpos() + + r = GDR() + w = r[0]/r[1] + h = 1 + + vd = sub(p2, s.p1) + vd_n = norm(vd) + + t_up = (0, 1, 0) + r_v = cross(vd_n, t_up) + r_v_n = norm(r_v) + + up_v = cross(r_v_n, vd_n) + up_v_n = norm(up_v) + + hw = w / 2.0 + hh = h / 2.0 + + tr = add(p2, add(scale(r_v_n, hw), scale(up_v_n, hh))) + tl = add(p2, add(scale(r_v_n, -hw), scale(up_v_n, hh))) + br = add(p2, add(scale(r_v_n, hw), scale(up_v_n, -hh))) + bl = add(p2, add(scale(r_v_n, -hw), scale(up_v_n, -hh))) + + s.line(s.p1, tr) + s.line(s.p1, tl) + s.line(s.p1, br) + s.line(s.p1, bl) + + m = 4 + j = {'tex': 'crossOutMask'} + s.line(tr, tl, m, **j) + s.line(tl, bl, m, **j) + s.line(bl, br, m, **j) + s.line(br, tr, m, **j) + + s.tip( + f'Your display\n{r[0]}x{r[1]} px\n{tuple([round(i, 2) for i in p2])}', tr, 'right') + s.stage = 2 + s.overlay.press(3) + tick(0.25, animate(s.node, 'mesh_scale', {0: 0.5, 0.1: 0.2, 0.2: 0.5}).delete) + gbs('gunCocking').play(position=s.node.position) + """Handle events""" + def handlemessage(s, m): + if isinstance(m, OutOfBoundsMessage): + p = s.getpos() + gbs('shatter').play(position=p) + emitfx( + scale=1, + count=30, + spread=0.1, + position=p, + chunk_type='ice' + ) + s.destroy() + note('Out of bounds!') + """Destroy""" + def destroy(s): + with ga().context: + n = s.node + s.mode = 2 + n.delete() + s.reset() + """Reset input""" + def reset(s): + s.__class__.__yes__ = False + me = getme() + if not me: + return + me.resetinput() + with ga().context: + me.actor.connect_controls_to_player() + [i.delete() for i in (s.kids+s.okay)] + """Manage movement""" + def manage(s, a, lr=0): + if lr: + s.llr = a + return + s.lud = a + """Move""" + def move(s): + m = getme(1) + if (not m) or m._dead: + s.destroy() + try: + p = s.getpos() + except: + s.overlay.destroy() + return + s.setpos((p[0]+s.llr*s.step, p[1], p[2]-s.lud*s.step)) + s.overlay.up(*p, s.llr, s.lud) + SCT(*p) + teck(s.wait, s.move) + """Start elevating""" + def start(s, i): + s.overlay.press(i) + s.mode = i + s.loop(i) + """Keep elevating""" + def loop(s, i): + if s.mode != i: + return + try: + p = list(s.node.position) + except: + return + p[1] += s.step if i else -s.step + s.node.position = tuple(p) + teck(s.wait, lambda: s.loop(i)) + """Stop elevating""" + def stop(s, i): + s.overlay.release(i) + s.mode = 4 + """Get position""" + def getpos(s): + return s.node.position + """Set position""" + def setpos(s, p): + s.node.position = p + """Done""" + def done(s, talk=True): + s.overlay.press(1) + s.overlay.destroy() + try: + p = s.node.position + except: + return + with ga().context: + gbs('laser').play(position=p) + tick(0.2, animate(s.node, 'mesh_scale', {0: 0.5, 0.08: 1, 0.2: 0}).delete) + tick(0.2, s.node.delete) + s.reset() + if s.stage > 1 and talk: + SCM(True) + SCP(*s.p1) + SCT(*s.p2) + var('lp1', s.p1) + var('lp2', s.p2) + nice('Applied!') + elif talk: + note('Incomplete camera setup\nNo changes applied.') + if s.__class__.__ins__ == s: + s.__class__.__ins__ = None + + +"""Controls overlay""" + + +class Overlay: + __lst__ = None + """Place nodes""" + def __init__(s): + s.__class__.__lst__ = str(ga()) + s.colors = [ + [(0.2, 0.6, 0.2), (0.4, 1, 0.4)], + [(0.6, 0, 0), (1, 0, 0)], + [(0.2, 0.6, 0.6), (0.4, 1, 1)], + [(0.6, 0.6, 0.2), (1, 1, 0.4)], + [(0.3, 0.23, 0.5), (0.2, 0.13, 0.3)] + ] + s.pics = [] + s.texts = [] + s.pos = [] + s.nub = [] + s.old = [0, 0, 0] + s.dead = False + with ga().context: + for i in range(4): + j = ['Jump', 'Bomb', 'PickUp', 'Punch'][i] + k = [600, 650, 600, 550][i] + l = [170, 220, 270, 220][i] + c = s.colors[i][0] + n = newnode( + 'image', + attrs={ + 'texture': gbt('button'+j), + 'absolute_scale': True, + 'position': (k, l), + 'scale': (60, 60), + 'color': c + } + ) + s.pics.append(n) + j = ['Down', 'Done', 'Up', 'Mark'][i] + k = [600, 680, 600, 515][i] + l = [115, 220, 325, 220][i] + h = ['center', 'left', 'center', 'right'][i] + v = ['bottom', 'center', 'top', 'center'][i] + n = newnode( + 'text', + attrs={ + 'text': j, + 'position': (k, l), + 'color': c, + 'h_align': h, + 'v_align': v + } + ) + s.texts.append(n) + for i in range(3): + c = s.colors[[1, 0, 2][i]][0] + n = newnode( + 'text', + attrs={ + 'text': '0', + 'position': (640, 155-30*i), + 'color': c, + 'h_align': 'left' + } + ) + s.pos.append(n) + s.np = (790, 140) + for i in [0, 1]: + j = [110, 60][i] + n = newnode( + 'image', + attrs={ + 'texture': gbt('nub'), + 'absolute_scale': True, + 'position': s.np, + 'scale': (j, j), + 'color': s.colors[4][i] + } + ) + s.nub.append(n) + s.fade() + """Color overlays""" + def set(s, i, c): + s.pics[i].color = s.texts[i].color = c + """Color position""" + def pset(s, i, c): + s.pos[i].color = c + """Simulate pressed""" + def press(s, i): + s.set(i, s.colors[i][1]) + s.pics[i].opacity = 1.0 + """Simulate released""" + def release(s, i): + s.set(i, s.colors[i][0]) + s.pics[i].opacity = 0.7 + """Get all nodes""" + def nodes(s): + return s.pics+s.texts+s.pos+s.nub + """Update position""" + def up(s, x, y, z, lr, ud): + new = [x, y, z] + for i in range(3): + c = s.colors[[1, 0, 2][i]] + if s.old[i] == new[i]: + s.pset(i, c[0]) + continue + t = s.pos[i] + t.text = str(round(new[i], 5)) + s.pset(i, c[1]) + s.old = new + [setattr(s.nub[i], 'opacity', [[0.5, 0.2], [0.7, 0.3]][bool(lr or ud)][i]) for i in [0, 1]] + p = s.np + m = sqrt(lr**2+ud**2) or 1 + d = 25*min(sqrt(lr**2+ud**2), 1) + lr /= m + ud /= m + s.nub[1].position = (p[0]+lr*d, p[1]+ud*d) + """Fade""" + def fade(s, i=0): + if str(ga()) != s.__class__.__lst__: + return + mem = s.nodes() + [tick(1, animate(n, 'opacity', {0: i, 0.5: abs(i-0.7)}).delete) for n in mem] + """Destroy overlay""" + def destroy(s): + if s.dead: + return + s.dead = True + with ga().context: + tick(0.2, lambda: s.fade(0.7)) + tick(2, lambda: [n.delete() for n in s.nodes()]) + + +# Mini tools +def note(t, b=False): return (push(t, color=(1, 1, 0)), gs('block').play() if b else None) +def nice(t): return (push(t, color=(0, 1, 0)), gs('dingSmallHigh').play()) +def SCM(b): return (setattr(Camera, '__lst__', b), SSCM(b)) +def scale(v, s): return (v[0]*s, v[1]*s, v[2]*s) +def cross(a, b): return (a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]) +def sub(a, b): return (a[0]-b[0], a[1]-b[1], a[2]-b[2]) +def add(a, b): return (a[0]+b[0], a[1]+b[1], a[2]+b[2]) + + +def getme(actor=0): + for p in ga().players: + if p.sessionplayer.inputdevice.client_id == -1: + return p.actor if actor else p + + +def LN(d): me = getme(); [me.assigninput(getattr(IT, k), d[k]) for k in d] + + +def RESUME(): + u = APP.ui_v1 + c = APP.classic + c.resume() + u.clear_main_window() + [z() for z in c.main_menu_resume_callbacks] + c.main_menu_resume_callbacks.clear() + + +def norm(v): + a, b, c = v + l = sqrt(a**2+b**2+c**2) + return (0, 0, 0) if l == 0 else (a/l, b/l, c/l) + + +def var(s, v=None): + c = APP.config + s = 'cam_'+s + if v is None: + return c.get(s, v) + c[s] = v + c.commit() + +# brobord collide grass +# ba_meta require api 9 +# ba_meta export babase.Plugin + + +class byBordd(Plugin): + def has_settings_ui(s): return True + def show_settings_ui(s, src): return s.ui(source=src) + col = (0.18, 0.18, 0.18) + + def __init__(s): + o = igm._refresh_in_game + + def e(f, *a, **k): + r = o(f, *a, **k) + b = bw( + label='', + size=(90, 40), + button_type='square', + parent=f._root_widget, + color=(0.18, 0.18, 0.18), + position=(f._width-20, 0), + ) + bw(b, on_activate_call=lambda: s.ui(source=b, main=True)) + iw( + size=(40, 40), + texture=gt('tv'), + parent=f._root_widget, + position=(f._width-20, 5) + ) + tw( + maxwidth=50, + text='Camera', + h_align='left', + parent=f._root_widget, + position=(f._width+15, 0) + ) + return r + igm._refresh_in_game = e + """The UI""" + def ui(s, source=None, main=False): + s.main = main + off = source.get_screen_space_center() if source else (0, 0) + w = cw( + color=s.col, + size=(350, 305), + stack_offset=off, + transition='in_scale', + parent=gsw('overlay_stack'), + scale_origin_stack_offset=off + ) + s.back = lambda b=True: ( + cw(w, transition=['out_right', 'out_scale'][bool(source) and b]), gs('swish').play() if b else None) + cw(w, on_outside_click_call=s.back) + b = Camera.__yes__ + t = [ + ('Camera is ready!', (0, 1, 1)), + ('Camera is running!', (0, 1, 0)), + ][b] + tw( + parent=w, + text=t[0], + scale=1.5, + color=t[1], + h_align='center', + position=(155, 250) + ) + for i in range(4): + j = [ + ('3D Camera mapper', s.start), + ('Last mapped config', s.load), + ('Last dev command', s.copy), + ('Reset all settings', s.reset) + ][i] + tw( + parent=w, + text=j[0], + maxwidth=195, + position=(20, 30+55*i) + ) + k = [ + (['Start', 'Stop'][b], 'cursor'), + ('Load', 'achievementOutline'), + ('Copy', 'file'), + ('Reset', 'replayIcon') + ][i] + bw( + parent=w, + label=k[0], + color=s.col, + size=(120, 50), + icon=gt(k[1]), + enable_sound=not i, + textcolor=(1, 1, 1), + button_type='square', + on_activate_call=j[1], + position=(220, 20+55*i) + ) + """Gather last""" + def gather(s): + return var('lp1'), var('lp2') + """Reset""" + def reset(s): + SCM(False) + nice('Resetored original settings!') + """Copy last""" + def copy(s): + if not CIS(): + note('Unsupported!', True) + return + g = s.gather() + if not g[1]: + note('Apply something first!', True) + return + g = [tuple([round(i, 2) for i in j]) for j in g] + COPY( + f'from _babase import set_camera_manual as SCM, set_camera_target as SCT, set_camera_position as SCP; SCM(True); SCP(*{g[0]}); SCT(*{g[1]})') + nice('Copied command!\nPaste it in dev console anytime to load config!') + """Load last""" + def load(s): + g = s.gather() + if not g[1]: + note('Apply something first!', True) + return + if Camera.__yes__: + note('Stop camera first!', True) + return + SCM(True) + SCP(*g[0]) + SCT(*g[1]) + nice('Loaded last config!') + """Start camera""" + def start(s): + a = ga() + if not a: + note('Only mapping requires you to be the host!\nYou still can load previous config though', True) + return + if not getme(): + note('Join the game first!', True) + return + s.back(False) + RESUME() if s.main else None + with a.context: + Camera() diff --git a/plugins/utilities/character_chooser.py b/plugins/utilities/character_chooser.py new file mode 100644 index 000000000..e0f0edf4a --- /dev/null +++ b/plugins/utilities/character_chooser.py @@ -0,0 +1,358 @@ +# ba_meta require api 9 + +''' +Character Chooser by Mr.Smoothy + +This plugin will let you choose your character from lobby. + +Install this plugin on your Phone/PC or on Server + +If installed on server :- this will also let players choose server specific custom characters . so no more sharing of character file with all players, +just install this plugin on server ...and players can pick character from lobby . + +Use:- +> select your profile (focus on color and name) +> press ready (punch) +> now use UP/DOWN buttons to scroll character list +> Press ready again (punch) to join the game +> or press Bomb button to go back to profile choosing menu +> END + +Watch : https://www.youtube.com/watch?v=hNmv2l-NahE +Join : https://discord.gg/ucyaesh +Contact : discord mr.smoothy#5824 + + +Share this plugin with your server owner /admins to use it online + + :) + +''' + + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +from bascenev1lib.actor.playerspaz import PlayerSpaz + + +import logging + +from babase._language import Lstr + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional +import weakref +import os +import json +from bascenev1._lobby import ChangeMessage, PlayerReadyMessage +from bascenev1 import _lobby +from bascenev1lib.actor.spazappearance import * + + +def __init__(self, vpos: float, sessionplayer: bs.SessionPlayer, + lobby: 'Lobby') -> None: + self._deek_sound = bs.getsound('deek') + self._click_sound = bs.getsound('click01') + self._punchsound = bs.getsound('punch01') + self._swish_sound = bs.getsound('punchSwish') + self._errorsound = bs.getsound('error') + self._mask_texture = bs.gettexture('characterIconMask') + self._vpos = vpos + self._lobby = weakref.ref(lobby) + self._sessionplayer = sessionplayer + self._inited = False + self._dead = False + self._text_node: Optional[bs.Node] = None + self._profilename = '' + self._profilenames: List[str] = [] + self._ready: bool = False + self._character_names: List[str] = [] + self._last_change: Sequence[Union[float, int]] = (0, 0) + self._profiles: Dict[str, Dict[str, Any]] = {} + + app = babase.app + + self.bakwas_chars = ["Lee", "Todd McBurton", "Zola", "Butch", "Witch", "warrior", + "Middle-Man", "Alien", "OldLady", "Gladiator", "Wrestler", "Gretel", "Robot"] + + # Load available player profiles either from the local config or + # from the remote device. + self.reload_profiles() + for name in bs.app.classic.spaz_appearances: + if name not in self._character_names and name not in self.bakwas_chars: + self._character_names.append(name) + # Note: this is just our local index out of available teams; *not* + # the team-id! + self._selected_team_index: int = self.lobby.next_add_team + + # Store a persistent random character index and colors; we'll use this + # for the '_random' profile. Let's use their input_device id to seed + # it. This will give a persistent character for them between games + # and will distribute characters nicely if everyone is random. + self._random_color, self._random_highlight = ( + bs.get_player_profile_colors(None)) + + # To calc our random character we pick a random one out of our + # unlocked list and then locate that character's index in the full + # list. + char_index_offset = app.classic.lobby_random_char_index_offset + self._random_character_index = ( + (sessionplayer.inputdevice.id + char_index_offset) % + len(self._character_names)) + + # Attempt to set an initial profile based on what was used previously + # for this input-device, etc. + self._profileindex = self._select_initial_profile() + self._profilename = self._profilenames[self._profileindex] + + self._text_node = bs.newnode('text', + delegate=self, + attrs={ + 'position': (-100, self._vpos), + 'maxwidth': 190, + 'shadow': 0.5, + 'vr_depth': -20, + 'h_align': 'left', + 'v_align': 'center', + 'v_attach': 'top' + }) + bs.animate(self._text_node, 'scale', {0: 0, 0.1: 1.0}) + self.icon = bs.newnode('image', + owner=self._text_node, + attrs={ + 'position': (-130, self._vpos + 20), + 'mask_texture': self._mask_texture, + 'vr_depth': -10, + 'attach': 'topCenter' + }) + + bs.animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)}) + + # Set our initial name to '' in case anyone asks. + self._sessionplayer.setname( + Lstr(resource='choosingPlayerText').evaluate(), real=False) + + # Init these to our rando but they should get switched to the + # selected profile (if any) right after. + self._character_index = self._random_character_index + self._color = self._random_color + self._highlight = self._random_highlight + self.characterchooser = False + self.update_from_profile() + self.update_position() + self._inited = True + + self._set_ready(False) + + +def _set_ready(self, ready: bool) -> None: + + # pylint: disable=cyclic-import + from babase._general import Call + profilename = self._profilenames[self._profileindex] + + # Handle '_edit' as a special case. + if profilename == '_edit' and ready: + with _babase.Context('ui'): + babase.app.classic.profile_browser_window() + # Give their input-device UI ownership too + # (prevent someone else from snatching it in crowded games) + _babase.set_ui_input_device(self._sessionplayer.inputdevice) + return + + if ready == False: + self._sessionplayer.assigninput( + babase.InputType.LEFT_PRESS, + Call(self.handlemessage, ChangeMessage('team', -1))) + self._sessionplayer.assigninput( + babase.InputType.RIGHT_PRESS, + Call(self.handlemessage, ChangeMessage('team', 1))) + self._sessionplayer.assigninput( + babase.InputType.BOMB_PRESS, + Call(self.handlemessage, ChangeMessage('character', 1))) + self._sessionplayer.assigninput( + babase.InputType.UP_PRESS, + Call(self.handlemessage, ChangeMessage('profileindex', -1))) + self._sessionplayer.assigninput( + babase.InputType.DOWN_PRESS, + Call(self.handlemessage, ChangeMessage('profileindex', 1))) + self._sessionplayer.assigninput( + (babase.InputType.JUMP_PRESS, babase.InputType.PICK_UP_PRESS, + babase.InputType.PUNCH_PRESS), + Call(self.handlemessage, ChangeMessage('ready', 1))) + self._ready = False + self._update_text() + self._sessionplayer.setname('untitled', real=False) + elif ready == True: + self.characterchooser = True + self._sessionplayer.assigninput( + (babase.InputType.LEFT_PRESS, babase.InputType.RIGHT_PRESS, + babase.InputType.UP_PRESS, babase.InputType.DOWN_PRESS, + babase.InputType.JUMP_PRESS, babase.InputType.BOMB_PRESS, + babase.InputType.PICK_UP_PRESS), self._do_nothing) + self._sessionplayer.assigninput( + (babase.InputType.UP_PRESS), Call(self.handlemessage, ChangeMessage('characterchooser', -1))) + self._sessionplayer.assigninput( + (babase.InputType.DOWN_PRESS), Call(self.handlemessage, ChangeMessage('characterchooser', 1))) + self._sessionplayer.assigninput( + (babase.InputType.BOMB_PRESS), Call(self.handlemessage, ChangeMessage('ready', 0))) + + self._sessionplayer.assigninput( + (babase.InputType.JUMP_PRESS, babase.InputType.PICK_UP_PRESS, babase.InputType.PUNCH_PRESS), + Call(self.handlemessage, ChangeMessage('ready', 2))) + + # Store the last profile picked by this input for reuse. + input_device = self._sessionplayer.inputdevice + name = input_device.name + unique_id = input_device.unique_identifier + device_profiles = _babase.app.config.setdefault( + 'Default Player Profiles', {}) + + # Make an exception if we have no custom profiles and are set + # to random; in that case we'll want to start picking up custom + # profiles if/when one is made so keep our setting cleared. + special = ('_random', '_edit', '__account__') + have_custom_profiles = any(p not in special + for p in self._profiles) + + profilekey = name + ' ' + unique_id + if profilename == '_random' and not have_custom_profiles: + if profilekey in device_profiles: + del device_profiles[profilekey] + else: + device_profiles[profilekey] = profilename + _babase.app.config.commit() + + # Set this player's short and full name. + self._sessionplayer.setname(self._getname(), + self._getname(full=True), + real=True) + self._ready = True + self._update_text() + else: + + # Inform the session that this player is ready. + bs.getsession().handlemessage(PlayerReadyMessage(self)) + + +def handlemessage(self, msg: Any) -> Any: + """Standard generic message handler.""" + + if isinstance(msg, ChangeMessage): + self._handle_repeat_message_attack() + + # If we've been removed from the lobby, ignore this stuff. + if self._dead: + logging.error('chooser got ChangeMessage after dying') + return + + if not self._text_node: + logging.error('got ChangeMessage after nodes died') + return + if msg.what == 'characterchooser': + self._click_sound.play() + # update our index in our local list of characters + self._character_index = ((self._character_index + msg.value) % + len(self._character_names)) + self._update_text() + self._update_icon() + + if msg.what == 'team': + sessionteams = self.lobby.sessionteams + if len(sessionteams) > 1: + self._swish_sound.play() + self._selected_team_index = ( + (self._selected_team_index + msg.value) % + len(sessionteams)) + self._update_text() + self.update_position() + self._update_icon() + + elif msg.what == 'profileindex': + if len(self._profilenames) == 1: + + # This should be pretty hard to hit now with + # automatic local accounts. + bui.getsound('error').play() + else: + + # Pick the next player profile and assign our name + # and character based on that. + self._deek_sound.play() + self._profileindex = ((self._profileindex + msg.value) % + len(self._profilenames)) + self.update_from_profile() + + elif msg.what == 'character': + self._click_sound.play() + self.characterchooser = True + # update our index in our local list of characters + self._character_index = ((self._character_index + msg.value) % + len(self._character_names)) + self._update_text() + self._update_icon() + + elif msg.what == 'ready': + self._handle_ready_msg(msg.value) + + +def _update_text(self) -> None: + assert self._text_node is not None + if self._ready: + + # Once we're ready, we've saved the name, so lets ask the system + # for it so we get appended numbers and stuff. + text = Lstr(value=self._sessionplayer.getname(full=True)) + if self.characterchooser: + text = Lstr(value='${A}\n${B}', + subs=[('${A}', text), + ('${B}', Lstr(value=""+self._character_names[self._character_index]))]) + self._text_node.scale = 0.8 + else: + text = Lstr(value='${A} (${B})', + subs=[('${A}', text), + ('${B}', Lstr(resource='readyText'))]) + else: + text = Lstr(value=self._getname(full=True)) + self._text_node.scale = 1.0 + + can_switch_teams = len(self.lobby.sessionteams) > 1 + + # Flash as we're coming in. + fin_color = _babase.safecolor(self.get_color()) + (1, ) + if not self._inited: + bs.animate_array(self._text_node, 'color', 4, { + 0.15: fin_color, + 0.25: (2, 2, 2, 1), + 0.35: fin_color + }) + else: + + # Blend if we're in teams mode; switch instantly otherwise. + if can_switch_teams: + bs.animate_array(self._text_node, 'color', 4, { + 0: self._text_node.color, + 0.1: fin_color + }) + else: + self._text_node.color = fin_color + + self._text_node.text = text + +# ba_meta export babase.Plugin + + +class HeySmoothy(babase.Plugin): + + def __init__(self): + _lobby.Chooser.__init__ = __init__ + _lobby.Chooser._set_ready = _set_ready + + _lobby.Chooser._update_text = _update_text + _lobby.Chooser.handlemessage = handlemessage diff --git a/plugins/utilities/character_maker.py b/plugins/utilities/character_maker.py new file mode 100644 index 000000000..aced6f8e8 --- /dev/null +++ b/plugins/utilities/character_maker.py @@ -0,0 +1,763 @@ +# Released under the MIT License. See LICENSE for details. + +# ba_meta require api 9 + + +''' +Character Builder/Maker by Mr.Smoothy +Plugin helps to mix character models and textures in interactive way. + +Watch tutorial : https://youtu.be/q0KxY1hfMPQ +Join discord: https://discord.gg/ucyaesh for help +https://bombsquad-community.web.app/home + +> create team playlist and add character maker mini game +> Use export command to save character +> Character will be saved in CustomCharacter folder inside Bombsquad Mods folder + + +Characters can be used offline or online +for online you need to share character file with server owners. + +*For server owners:_ + You might know what to do with that file, + Still , + refer code after line 455 in this file , add it as a plugin to import characters from json file. + +*For modders:- + You can add more models and texture , check line near 400 and add asset names , you can also modify sounds and icon in json file (optional) . + +To share your character with friends , + send them character .json file and tell them to put file in same location i.e mods/CustomCharacter or for PC appdata/Local/Bombsquad/Mods/CustomCharacter + this plugin should be installed on their device too. + +Dont forget to share your creativity with me , +send your character screenshot discord: mr.smoothy#5824 https://discord.gg/ucyaesh + +Register your character in above discord server , so other server owners can add your characters. + + +Released on 28 May 2021 +Update 2 june : use import +Update 29 July 2023: updated to API 8 , multiplayer support +''' + +from typing import Sequence +import _babase +import babase +import bauiv1 as bui +from bascenev1lib.actor.spazappearance import * +from bascenev1lib.actor.text import Text +from bascenev1lib.actor.image import Image +import bauiv1lib.mainmenu + +import os +import copy +import json + +GAME_USER_DIRECTORY = _babase.env()["python_directory_user"] +CUSTOM_CHARACTERS = os.path.join(GAME_USER_DIRECTORY, "CustomCharacters") +os.makedirs(CUSTOM_CHARACTERS, exist_ok=True) + + +SPAZ_PRESET = { + "color_mask": "neoSpazColorMask", + "color_texture": "neoSpazColor", + "head": "neoSpazHead", + "hand": "neoSpazHand", + "torso": "neoSpazTorso", + "pelvis": "neoSpazTorso", + "upper_arm": "neoSpazUpperArm", + "forearm": "neoSpazForeArm", + "upper_leg": "neoSpazUpperLeg", + "lower_leg": "neoSpazLowerLeg", + "toes_mesh": "neoSpazToes", + "jump_sounds": ['spazJump01', 'spazJump02', 'spazJump03', 'spazJump04'], + "attack_sounds": ['spazAttack01', 'spazAttack02', 'spazAttack03', 'spazAttack04'], + "impact_sounds": ['spazImpact01', 'spazImpact02', 'spazImpact03', 'spazImpact04'], + "death_sounds": ['spazDeath01'], + "pickup_sounds": ['spazPickup01'], + "fall_sounds": ['spazFall01'], + "icon_texture": "neoSpazIcon", + "icon_mask_texture": "neoSpazIconColorMask", + "style": "spaz" +} + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self): + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class CharacterBuilder(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Character Maker' + description = 'Create your own custom Characters' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings(cls, sessiontype): + settings = [ + bs.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + + if issubclass(sessiontype, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype): + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype): + return ['Rampage'] + + def __init__(self, settings): + super().__init__(settings) + + self.initialize_meshs() + self._score_to_win = None + self._dingsound = bs.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + + self._punch_image = Image( + bs.gettexture('buttonPunch'), + position=(345, 200), + scale=(50, 50), + color=(0.9, 0.9, 0, 0.9) + ) + self._punch_text = Text( + "Model+", + scale=0.7, + shadow=0.5, + flatness=0.5, + color=(0.9, 0.9, 0, 0.9), + position=(263, 190)) + + self._grab_image = Image( + bs.gettexture('buttonPickUp'), + position=(385, 240), + scale=(50, 50), + color=(0, 0.7, 0.9) + ) + self._grab_text = Text( + "Component-", + scale=0.7, + shadow=0.5, + flatness=0.5, + color=(0, 0.7, 1, 0.9), + position=(340, 265)) + + self._jump_image = Image( + bs.gettexture('buttonJump'), + position=(385, 160), + scale=(50, 50), + color=(0.2, 0.9, 0.2, 0.9) + ) + self._jump_text = Text( + "Component+", + scale=0.7, + shadow=0.5, + flatness=0.5, + color=(0.2, 0.9, 0.2, 0.9), + position=(340, 113)) + + self._bomb_image = Image( + bs.gettexture('buttonBomb'), + position=(425, 200), + scale=(50, 50), + color=(0.9, 0.2, 0.2, 0.9) + ) + self._bomb_text = Text( + "Model-", + scale=0.7, + shadow=0.5, + flatness=0.5, + color=(0.9, 0.2, 0.2, 0.9), + position=(452, 190)) + + self._host = Text( + "Originally created by \ue020HeySmoothy\nhttps://youtu.be/q0KxY1hfMPQ\nhttps://youtu.be/3l2dxWEhrzE\n\nModified for multiplayer by \ue047Nyaa! :3", + flash=False, + maxwidth=0, + scale=0.65, + shadow=0.5, + flatness=0.5, + h_align=Text.HAlign.RIGHT, + v_attach=Text.VAttach.BOTTOM, + h_attach=Text.HAttach.RIGHT, + color=(1.0, 0.4, 0.95, 0.8), + position=(-2, 82)) + self._discord = Text( + "Join discord.gg/ucyaesh to provide feedback or to use this Character Maker offline!", + flash=False, + maxwidth=0, + scale=0.85, + shadow=0.5, + flatness=0.5, + h_align=Text.HAlign.CENTER, + v_attach=Text.VAttach.BOTTOM, + h_attach=Text.HAttach.CENTER, + color=(1.0, 0.4, 0, 1), + position=(0, 110)) + self._website = Text( + "check mods folder to get JSON character file ever exported from here!", + flash=False, + maxwidth=0, + scale=0.85, + shadow=0.5, + flatness=0.5, + h_align=Text.HAlign.CENTER, + v_attach=Text.VAttach.BOTTOM, + h_attach=Text.HAttach.CENTER, + color=(0.2, 0.9, 1.0, 1), + position=(0, 150)) + self._commands = Text( + "Commands:\n\n\t1. /info\n\t2. /export \n\t3. /import \n\t", + flash=False, + maxwidth=0, + scale=0.8, + shadow=0.5, + flatness=0.5, + h_align=Text.HAlign.LEFT, + v_attach=Text.VAttach.TOP, + h_attach=Text.HAttach.LEFT, + color=(0.3, 0.9, 0.3, 0.8), + position=(30, -112)) + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = bs.MusicType.MARCHING + + def get_instance_description(self): + return '' + + def get_instance_description_short(self): + return '' + + def on_team_join(self, team: Team): + if self.has_begun(): + pass + + def on_begin(self): + super().on_begin() + + def nextBodyPart(self, spaz): + spaz.bodyindex = (spaz.bodyindex+1) % len(self.cache.keys()) + try: + spaz.bodypart.delete() + except AttributeError: + pass + part = list(self.cache.keys())[spaz.bodyindex] + spaz.bodypart = bs.newnode( + 'text', + owner=spaz.node, + attrs={ + 'text': str(part), + 'in_world': True, + 'color': (1, 1, 1), + 'scale': 0.011, + 'shadow': 0.5, + 'flatness': 0.5, + 'h_align': 'center', }) + math = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.7, 0.5), + 'operation': 'add', + }) + spaz.node.connectattr('position', math, 'input2') + math.connectattr('output', spaz.bodypart, 'position') + bs.getsound('deek').play() + + def prevBodyPart(self, spaz): + spaz.bodyindex = (spaz.bodyindex-1) % len(self.cache.keys()) + try: + spaz.bodypart.delete() + except AttributeError: + pass + part = list(self.cache.keys())[spaz.bodyindex] + spaz.bodypart = bs.newnode( + 'text', + owner=spaz.node, + attrs={ + 'text': str(part), + 'in_world': True, + 'color': (1, 1, 1), + 'scale': 0.011, + 'shadow': 0.5, + 'flatness': 0.5, + 'h_align': 'center', }) + math = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.7, 0.5), + 'operation': 'add', + }) + spaz.node.connectattr('position', math, 'input2') + math.connectattr('output', spaz.bodypart, 'position') + bs.getsound('deek').play() + + def nextModel(self, spaz): + try: + spaz.newmesh.delete() + except AttributeError: + pass + part = list(self.cache.keys())[spaz.bodyindex] + spaz.meshindex = (spaz.meshindex+1) % len(self.cache[part]) + mesh = self.cache[part][spaz.meshindex] + spaz.newmesh = bs.newnode( + 'text', + owner=spaz.node, + attrs={ + 'text': str(mesh), + 'in_world': True, + 'color': (1, 1, 1), + 'scale': 0.011, + 'shadow': 0.5, + 'flatness': 0.5, + 'h_align': 'center' + }) + math = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, -0.6, 0.5), + 'operation': 'add', + }) + spaz.node.connectattr('position', math, 'input2') + math.connectattr('output', spaz.newmesh, 'position') + if part == "main_color": + self.setColor(spaz, mesh) + elif part == "highlight_color": + self.setHighlight(spaz, mesh) + else: + self.setModel(spaz, part, mesh) + bs.getsound('click01').play() + + def prevModel(self, spaz): + try: + spaz.newmesh.delete() + except AttributeError: + pass + part = list(self.cache.keys())[spaz.bodyindex] + spaz.meshindex = (spaz.meshindex-1) % len(self.cache[part]) + mesh = self.cache[part][spaz.meshindex] + spaz.newmesh = bs.newnode( + 'text', + owner=spaz.node, + attrs={ + 'text': str(mesh), + 'in_world': True, + 'color': (1, 1, 1), + 'scale': 0.011, + 'shadow': 0.5, + 'flatness': 0.5, + 'h_align': 'center' + }) + math = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, -0.6, 0.5), + 'operation': 'add', + }) + spaz.node.connectattr('position', math, 'input2') + math.connectattr('output', spaz.newmesh, 'position') + if part == "main_color": + self.setColor(spaz, mesh) + elif part == "highlight_color": + self.setHighlight(spaz, mesh) + else: + self.setModel(spaz, part, mesh) + bs.getsound('click01').play() + + def setColor(self, spaz, color): + spaz.node.color = color + + def setHighlight(self, spaz, highlight): + spaz.node.highlight = highlight + + def setModel(self, spaz, bodypart, meshname): + if bodypart == 'head': + spaz.node.head_mesh = bs.getmesh(meshname) + elif bodypart == 'torso': + spaz.node.torso_mesh = bs.getmesh(meshname) + elif bodypart == 'pelvis': + spaz.node.pelvis_mesh = bs.getmesh(meshname) + elif bodypart == 'upper_arm': + spaz.node.upper_arm_mesh = bs.getmesh(meshname) + elif bodypart == 'forearm': + spaz.node.forearm_mesh = bs.getmesh(meshname) + elif bodypart == 'hand': + spaz.node.hand_mesh = bs.getmesh(meshname) + elif bodypart == 'upper_leg': + spaz.node.upper_leg_mesh = bs.getmesh(meshname) + elif bodypart == 'lower_leg': + spaz.node.lower_leg_mesh = bs.getmesh(meshname) + elif bodypart == 'toes_mesh': + spaz.node.toes_mesh = bs.getmesh(meshname) + elif bodypart == 'style': + spaz.node.style = meshname + elif bodypart == 'color_texture': + spaz.node.color_texture = bs.gettexture(meshname) + elif bodypart == 'color_mask': + spaz.node.color_mask_texture = bs.gettexture(meshname) + + def spawn_player(self, player): + spaz = self.spawn_player_spaz(player) + spaz.bodyindex = 0 + spaz.meshindex = 0 + spaz.bodypart = bs.newnode( + 'text', + owner=spaz.node, + attrs={ + 'text': "", + 'in_world': True, + 'scale': 0.011, + 'color': (1, 0.4, 0.9, 1), + 'h_align': 'center', + 'shadow': 0.7, + 'flatness': 0.5, + }) + math = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.7, 0.5), + 'operation': 'add', + }) + spaz.node.connectattr('position', math, 'input2') + math.connectattr('output', spaz.bodypart, 'position') + spaz.newmesh = bs.newnode( + 'text', + owner=spaz.node, + attrs={ + 'text': "", + 'in_world': True, + 'scale': 0.011, + 'color': (1, 0.4, 0.9, 1), + 'h_align': 'center', + 'shadow': 0.7, + 'flatness': 0.5, + }) + math = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, -0.6, 0.5), + 'operation': 'add', + }) + spaz.node.connectattr('position', math, 'input2') + math.connectattr('output', spaz.newmesh, 'position') + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + # spaz.connect_controls_to_player(enable_punch=False, + # enable_jump=False, + # enable_bomb=False, + # enable_pickup=False) + + intp = babase.InputType + player.assigninput(intp.JUMP_PRESS, lambda: self.nextBodyPart(spaz)) + player.assigninput(intp.PICK_UP_PRESS, lambda: self.prevBodyPart(spaz)) + player.assigninput(intp.PUNCH_PRESS, lambda: self.nextModel(spaz)) + player.assigninput(intp.BOMB_PRESS, lambda: self.prevModel(spaz)) + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + def handlemessage(self, msg): + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + self.respawn_player(player) + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self): + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self): + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def initialize_meshs(self): + self.cache = { + "head": ["bomb", "landMine", "wing", "eyeLid", "impactBomb"], + "hand": ["hairTuft3", "bomb", "powerup"], + "torso": ["bomb", "landMine", "bomb"], + "pelvis": ["hairTuft4", "bomb"], + "upper_arm": ["wing", "locator", "bomb"], + "forearm": ["flagPole", "bomb"], + "upper_leg": ["bomb"], + "lower_leg": ["bomb"], + "toes_mesh": ["bomb"], + "style": ["spaz", "female", "ninja", "kronk", "mel", "pirate", "santa", "frosty", "bones", "bear", "penguin", "ali", "cyborg", "agent", "pixie", "bunny"], + "color_texture": ["kronk", "egg1", "egg2", "egg3", "achievementGotTheMoves", "bombColor", "crossOut", "explosion", "rgbStripes", "powerupCurse", "powerupHealth", "impactBombColorLit"], + "color_mask": ["egg1", "egg2", "egg3", "bombColor", "crossOutMask", "fontExtras3"], + "main_color": [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (1, 0, 1), (0, 1, 1), (1, 1, 1)], + "highlight_color": [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (1, 0, 1), (0, 1, 1), (1, 1, 1)], + } + chars = ["neoSpaz", "zoe", "ninja", "kronk", "mel", "jack", "santa", "frosty", + "bones", "bear", "penguin", "ali", "cyborg", "agent", "wizard", "pixie", "bunny"] + + for char in chars: + self.cache["head"].append(char + "Head") + self.cache["hand"].append(char + "Hand") + self.cache["torso"].append(char + "Torso") + if char not in ['mel', "jack", "santa"]: + self.cache["pelvis"].append(char + "Pelvis") + self.cache["upper_arm"].append(char + "UpperArm") + self.cache["forearm"].append(char + "ForeArm") + self.cache["upper_leg"].append(char + "UpperLeg") + self.cache["lower_leg"].append(char + "LowerLeg") + self.cache["toes_mesh"].append(char + "Toes") + self.cache["color_mask"].append(char + "ColorMask") + if char != "kronk": + self.cache["color_texture"].append(char + "Color") + + +def mesh_to_string(mesh): + return str(mesh)[17:-2] + + +def texture_to_string(texture): + return str(texture)[20:-2] + + +def spaz_to_json(spaz): + spaz_json = copy.deepcopy(SPAZ_PRESET) + spaz_json['head'] = mesh_to_string(spaz.node.head_mesh) + spaz_json['hand'] = mesh_to_string(spaz.node.hand_mesh) + spaz_json['torso'] = mesh_to_string(spaz.node.torso_mesh) + spaz_json['pelvis'] = mesh_to_string(spaz.node.pelvis_mesh) + spaz_json['upper_arm'] = mesh_to_string(spaz.node.upper_arm_mesh) + spaz_json['forearm'] = mesh_to_string(spaz.node.forearm_mesh) + spaz_json['upper_leg'] = mesh_to_string(spaz.node.upper_leg_mesh) + spaz_json['lower_leg'] = mesh_to_string(spaz.node.lower_leg_mesh) + spaz_json['toes_mesh'] = mesh_to_string(spaz.node.toes_mesh) + spaz_json['style'] = spaz.node.style + spaz_json['color_mask'] = texture_to_string(spaz.node.color_mask_texture) + spaz_json['color_texture'] = texture_to_string(spaz.node.color_texture) + return spaz_json + + +def import_character(name, spaz): + if not name: + bs.screenmessage("Inavlid character name") + return + character = None + for appearance_name, appearance_character in bs.app.classic.spaz_appearances.items(): + if name.lower() == appearance_name.lower(): + character = appearance_character + break + if not character: + return (False, name) + activity = bs.get_foreground_host_activity() + with activity.context: + spaz.node.head_mesh = bs.getmesh(character.head_mesh) + spaz.node.hand_mesh = bs.getmesh(character.hand_mesh) + spaz.node.torso_mesh = bs.getmesh(character.torso_mesh) + spaz.node.pelvis_mesh = bs.getmesh(character.pelvis_mesh) + spaz.node.upper_arm_mesh = bs.getmesh(character.upper_arm_mesh) + spaz.node.forearm_mesh = bs.getmesh(character.forearm_mesh) + spaz.node.upper_leg_mesh = bs.getmesh(character.upper_leg_mesh) + spaz.node.lower_leg_mesh = bs.getmesh(character.lower_leg_mesh) + spaz.node.toes_mesh = bs.getmesh(character.toes_mesh) + spaz.node.style = character.style + spaz.node.color_mask_texture = bs.gettexture(character.color_mask_texture) + spaz.node.color_texture = bs.gettexture(character.color_texture) + return (True, appearance_name) + + +def export_character(name, spaz): + default_characters = tuple(bs.app.classic.spaz_appearances.keys())[:30] + os.makedirs(CUSTOM_CHARACTERS, exist_ok=True) + character_file = name + ".json" + for saved_character_file in os.listdir(CUSTOM_CHARACTERS): + if character_file.lower() == saved_character_file.lower(): + return (False, os.path.splitext(saved_character_file)[0]) + for default_character in default_characters: + if name.lower() == default_character.lower(): + return (False, default_character) + spaz_json = spaz_to_json(spaz) + with open(os.path.join(CUSTOM_CHARACTERS, character_file), "w") as fout: + json.dump(spaz_json, fout, indent=4) + register_character_json(name, spaz_json) + return (True, name) + + +def register_character_json(name, character): + appearance = Appearance(name) + appearance.color_texture = character['color_texture'] + appearance.color_mask_texture = character['color_mask'] + appearance.default_color = (0.6, 0.6, 0.6) + appearance.default_highlight = (0, 1, 0) + appearance.icon_texture = character['icon_texture'] + appearance.icon_mask_texture = character['icon_mask_texture'] + appearance.head_mesh = character['head'] + appearance.torso_mesh = character['torso'] + appearance.pelvis_mesh = character['pelvis'] + appearance.upper_arm_mesh = character['upper_arm'] + appearance.forearm_mesh = character['forearm'] + appearance.hand_mesh = character['hand'] + appearance.upper_leg_mesh = character['upper_leg'] + appearance.lower_leg_mesh = character['lower_leg'] + appearance.toes_mesh = character['toes_mesh'] + appearance.jump_sounds = character['jump_sounds'] + appearance.attack_sounds = character['attack_sounds'] + appearance.impact_sounds = character['impact_sounds'] + appearance.death_sounds = character['death_sounds'] + appearance.pickup_sounds = character['pickup_sounds'] + appearance.fall_sounds = character['fall_sounds'] + appearance.style = character['style'] + + +cm = bs.chatmessage + + +def _new_chatmessage(msg: str | babase.Lstr, *args, **kwargs): + activity = bs.get_foreground_host_activity() + if not activity: + cm(msg, *args, **kwargs) + return + + is_a_command = any(msg.startswith(command) for command in ("/export", "/import", "/info")) + if not is_a_command: + cm(msg, *args, **kwargs) + return + + player = get_player(msg, activity) + if not player: + cm("no player exists in game, try adding client id at last of command", *args, **kwargs) + cm(msg, *args, **kwargs) + return + + if msg.startswith("/export"): + if len(msg.split(" ")) > 1: + success, character_name = export_character(" ".join(msg.split(" ")[1:]), player.actor) + if success: + bs.screenmessage( + 'Exported character "{}"'.format(character_name), + color=(0, 1, 0) + ) + bui.getsound("gunCocking").play() + else: + bs.screenmessage( + 'Character "{}" already exists'.format(character_name), + color=(1, 0, 0) + ) + bui.getsound("error").play() + else: + cm("Enter name of character, Usage: /export ", *args, **kwargs) + elif msg.startswith("/import"): + if len(msg.split(" ")) > 1: + success, character_name = import_character(" ".join(msg.split(" ")[1:]), player.actor) + if success: + bs.screenmessage( + 'Imported character "{}"'.format(character_name), + color=(0, 1, 0) + ) + bui.getsound("gunCocking").play() + else: + bs.screenmessage( + 'Character "{}" doesn\'t exist'.format(character_name), + color=(1, 0, 0) + ) + bui.getsound("error").play() + else: + cm("Usage: /import ", *args, **kwargs) + elif msg.startswith("/info"): + spaz_json = spaz_to_json(player.actor) + del spaz_json["jump_sounds"] + del spaz_json["attack_sounds"] + del spaz_json["impact_sounds"] + del spaz_json["death_sounds"] + del spaz_json["pickup_sounds"] + del spaz_json["fall_sounds"] + spaz_str = "" + for key, value in spaz_json.items(): + spaz_str += "{}: {}\n".format(key, value) + bs.screenmessage(spaz_str, color=(1, 1, 1)) + + cm(msg, *args, **kwargs) + + +bs.chatmessage = _new_chatmessage + + +def get_player(msg, activity): + client_id = -1 + words = msg.split(" ") + last_word = words[-1] + if last_word.isdigit(): + client_id = int(last_word) + for player in activity.players: + player_client_id = player.sessionplayer.inputdevice.client_id + if client_id == player_client_id: + return player + + +# ba_meta export babase.Plugin +class bySmoothy(babase.Plugin): + def __init__(self): + _babase.import_character = import_character + _babase.export_character = export_character + _babase.spaz_to_json = spaz_to_json + character_files = os.listdir(CUSTOM_CHARACTERS) + for character_file in character_files: + if character_file.lower().endswith(".json"): + name, _ = os.path.splitext(character_file) + with open(os.path.join(CUSTOM_CHARACTERS, character_file), "r") as fin: + character = json.load(fin) + register_character_json(name, character) diff --git a/plugins/utilities/chat_cmd.py b/plugins/utilities/chat_cmd.py new file mode 100644 index 000000000..1fa80e9f6 --- /dev/null +++ b/plugins/utilities/chat_cmd.py @@ -0,0 +1,566 @@ +# Ported by brostos to api 8 +# Tool used to make porting easier.(https://github.com/bombsquad-community/baport) +"""python 3.9 | chatcmd for a beutiful game - BombSquad OwO""" +# modded by IM_NOT_PRANAV#7874 + +# biggggggg thankssssssssssssss to FireFighter1037 for helping everything + +# -*- coding: utf-8 -*- +# Ported by brostos to api 8 +# ba_meta require api 8 + +import threading +import time +from bascenev1 import get_foreground_host_activity, get_foreground_host_session, get_game_roster, get_chat_messages, chatmessage as cmsg +from bauiv1 import set_party_icon_always_visible, screenmessage as smsg +import babase +import bauiv1 as bui +import bascenev1 as bs +from bauiv1lib import mainmenu + +# our prefix that what we starts cmds with +px = '/' + +# main class + + +class _cmds: + + def _process_cmd(): + try: + messages = get_chat_messages() + if len(messages) > 1: + lastmsg = messages[len(messages)-1] + + m = lastmsg.split(' ')[1] + if m.startswith(px): + return _cmds._handle() + else: + pass + except: + pass + + def _handle(): + messages = get_chat_messages() + if len(messages) > 1: + lastmsg = messages[len(messages)-1] + + m = lastmsg.split(' ')[1] # cmd + n = lastmsg.split(' ')[2:] # aguments + + roster = get_game_roster() + session = get_foreground_host_session() + session_players = session.sessionplayers + + activity = get_foreground_host_activity() + activity_players = activity.players + + if m == px: + cmsg(px+'help for help') + + elif m == px+'help': + if n == []: + cmsg('===========================================') + cmsg(f' {px}help 1 - for page 1 | simple commands') + cmsg(f' {px}help 2 - for page 2 | all or number of list cmds') + cmsg(f' {px}help 3 - for page 3 | Other useful cmds') + cmsg('===========================================') + elif n[0] == '1': + cmsg('============================') + cmsg(f' {px}help 1 page 1 |') + cmsg(f' {px}help 1 page 2 |') + cmsg('============================') + if n[1] in ['page', 'Page', ]: + if n[2] == '1': + cmsg('============== Help 1, Page 1 ==============') + cmsg(f' your command prefix is or all commands starts with - {px}') + cmsg(f' {px}list or {px}l -- to see ids of players and execute commands') + cmsg(f' {px}uniqeid or {px}id -- to see accountid/uniqeid of player') + cmsg(f' {px}quit or {px}restart -- to restart the game') + cmsg(f' {px}mute/unmute -- to mute chat for everyone in your game') + elif n[2] == '2': + cmsg('============== Help 1, Page 2 ==============') + cmsg(f' {px}pause -- to pause everyone in your game') + cmsg(f' {px}nv or {px}night -- to make night in your game') + cmsg(f' {px}dv or {px}day -- to make night in your game') + cmsg(f' {px}camera_mode -- to rotate camera ,repat to off') + cmsg('===========================================') + elif n[0] == '2': + cmsg('============================') + cmsg(f' {px}help 2 page 1 |') + cmsg(f' {px}help 2 page 2 |') + cmsg(f' {px}help 2 page 3 |') + cmsg('============================') + if n[1] in ['page', 'Page']: + if n[2] == '1': + cmsg('============== Help 2 Page 1 ==============') + cmsg(f' {px}kill all or {px}kill number of list | kills the player') + cmsg(f' {px}heal all or {px}heal number of list | heals the players') + cmsg(f' {px}freeze all or {px}freeze number of list | freeze the player') + cmsg( + f' {px}unfreeze/thaw all or {px}unfreeze/thaw number of list | unfreeze the player') + cmsg(f' {px}gloves all or {px}gloves number of list | give gloves to player') + cmsg('============================') + elif n[2] == '2': + cmsg('============== Help 2 Page 2 ==============') + cmsg(f' {px}shield all or {px}shield number of list | give shield the player') + cmsg( + f' {px}fall all or {px}fall number of list | teleport in down and fall up the player') + cmsg(f' {px}curse all or {px}curse number of list | curse the player') + cmsg( + f' {px}creepy all or {px}creepy number of list | make creepy actor of player') + cmsg(f' {px}inv all or {px}inv number of list | makes invisible player') + cmsg( + f' {px}celebrate all or {px}celebrate number of list | celebrate action to the player') + cmsg('============================') + elif n[2] == '3': + cmsg('============== Help 2 Page 3 ==============') + cmsg(f' {px}gm all or {px}gm number of list | give bs gods like powers to player') + cmsg( + f' {px}sp all or {px}sp number of list | give superrrrrrr damages when punch to player') + cmsg(f' {px}sleep all or {px}sleep number of list | sleep up the player') + cmsg(f' {px}fly all or {px}fly number of list | fly up the player ') + cmsg(f' {px}hug number of list | hugup the player') + cmsg('============================') + + elif n[0] == '3': + cmsg('============================') + cmsg(f" {px}d_bomb bombType | changes default bomb | do {px}d_bomb help for bomb names ") + cmsg(f' {px}dbc (number of bombs) | changes default count of player') + cmsg('============================') + + elif m in [px+'list', px+'l', px+'clientids', px+'ids', px+'playerids']: + cmsg('======= Indexs ======') + for i in session_players: + cmsg(i.getname()+' --> '+str(session_players.index(i))+'\n') + if not roster == []: + for i in roster: + cmsg(f'======For {px}kick only======') + cmsg(str(i['players'][0]['nam_full'])+' - '+str(i['client_id'])) + + elif m in [px+'uniqeid', px+'id', px+'pb-id', px+'pb', px+'accountid']: + if n == []: + cmsg(f'use : {px}uniqeid number of list') + else: + try: + id = session_players[int(n[0])] + cmsg(id.getname()+"'s accountid is "+id.get_v1_account_id()) + except: + cmsg('could not found player') + + elif m in [px+'quit', px+'restart']: + babase.quit() + + elif m in [px+'mute', px+'mutechat']: + cfg = babase.app.config + cfg['Chat Muted'] = True + cfg.apply_and_commit() + cmsg('muted') + smsg(f'chat muted use {px}unmute and click on send to unmute') + + elif m in [px+'unmute', px+'unmutechat']: + cfg = babase.app.config + cfg['Chat Muted'] = False + cfg.apply_and_commit() + cmsg('un_muted') + smsg('chat un_muted') + + elif m in [px+'end', px+'next']: + if n == []: + try: + activity.end_game() + cmsg('Game ended Hope you scored great') + except: + cmsg('Game already ended') + + elif m in [px+'dv', px+'day']: + if activity.globalsnode.tint == (1.0, 1.0, 1.0): + cmsg(f'alwardy {px}dv is on ,do {px}nv for night') + else: + activity.globalsnode.tint = (1.0, 1.0, 1.0) + cmsg('day mode on!') + + elif m in [px+'nv', px+'night']: + if activity.globalsnode.tint == (0.5, 0.7, 1.0): + cmsg(f'alwardy {px}nv is on ,do {px}dv for day') + else: + activity.globalsnode.tint = (0.5, 0.7, 1.0) + cmsg('night mode on!') + + elif m in [px+'sm', px+'slow', px+'slowmo']: + if n == []: + if not activity.globalsnode.slow_motion: + activity.globalsnode.slow_motion = True + cmsg('Game in Epic Mode Now') + else: + activity.globalsnode.slow_motion = False + cmsg('Game in normal mode now ') + + elif m in [px+'pause', px+'pausegame']: + if n == []: + if not activity.globalsnode.paused: + activity.globalsnode.paused = True + cmsg('Game Paused') + else: + activity.globalsnode.paused = False + cmsg('Game un paused') + + elif m in [px+'cameraMode', px+'camera_mode', px+'rotate_camera']: + if n == []: + if not activity.globalsnode.camera_mode == 'rotate': + activity.globalsnode.camera_mode = 'rotate' + cmsg('camera mode is rotate now') + else: + activity.globalsnode.camera_mode = 'follow' + cmsg('camera mode is normal now') + + elif m in [px+'remove', px+'rm']: + if n == []: + cmsg(f'{px}remove all or {px}remove number in list') + elif n[0] == 'all': + for i in session_players: + i.remove_from_game() + cmsg('Removed All') + else: + try: + r = session_players[int(n[0])] + r.remove_from_game() + cmsg('Removed') # cant use getname() activity alwrady finish + except: + cmsg('could not found player') + elif m in [px+'inv', px+'invisible']: + if n == []: + cmsg(f'help : {px}inv all or {px}inv number of list') + elif n[0] == 'all': + for i in activity_players: + body = i.actor.node + if not body.torso_mesh == None: + body.head_mesh = None + body.torso_mesh = None + body.upper_arm_mesh = None + body.forearm_mesh = None + body.pelvis_mesh = None + body.hand_mesh = None + body.toes_mesh = None + body.upper_leg_mesh = None + body.lower_leg_mesh = None + body.style = 'cyborg' + cmsg('All invisible now Dont get cought') + else: + cmsg('alwardy invisible') + else: + body = activity_players[int(n[0])].actor.node + is_name = session_players[int(n[0])].getname() + if not body.torso_mesh == None: + body.head_mesh = None + body.torso_mesh = None + body.upper_arm_mesh = None + body.forearm_mesh = None + body.pelvis_mesh = None + body.hand_mesh = None + body.toes_mesh = None + body.upper_leg_mesh = None + body.lower_leg_mesh = None + body.style = 'cyborg' + cmsg(is_name+' using invisiblelity ') + else: + cmsg('alwardy invisible') + + elif m in [px+'hl', px+'headless']: + if n == []: + cmsg(f'help : {px}spaz all or {px}spaz number of list') + elif n[0] == 'all': + for i in activity_players: + body = i.actor.node + if not body.head_mesh == None: + body.head_mesh = None + body.style = 'cyborg' + cmsg('headless ? xD') + else: + cmsg('alwardy headless are you really headless?') + else: + body = activity_players[int(n[0])].actor.node + is_name = session_players[int(n[0])].getname() + if not body.head_mesh == None: + body.head_mesh = None + body.style = 'cyborg' + cmsg(is_name+'is headless now xD') + else: + cmsg('alwardy headless are you really headless?') + + elif m in [px+'creepy', px+'creep']: + if n == []: + cmsg(f'use: {px}creepy all or {px}creepy number of list') + elif n[0] == 'all': + for i in activity_players: + body = i.actor.node + body.head_mesh = None + body.handlemessage(bs.PowerupMessage(poweruptype='punch')) + body.handlemessage(bs.PowerupMessage(poweruptype='shield')) + cmsg('dont creep out childs all will be scared') + else: + try: + body = activity_players[int(n[0])].actor.node + body.head_mesh = None + body.handlemessage(bs.PowerupMessage(poweruptype='punch')) + body.handlemessage(bs.PowerupMessage(poweruptype='shield')) + cmsg('dont creep out childs all will be scared') + except: + cmsg('could not found player to make') + + elif m in [px+'kill', px+'die']: + if n == []: + cmsg(f'Use : {px}kill all or {px}kill number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(bs.DieMessage()) + cmsg('Killed all') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage(bs.DieMessage()) + cmsg('Killed '+is_name) + + elif m in [px+'heal', px+'heath']: + if n == []: + cmsg(f'Use: {px}heal all or {px}heal number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(bs.PowerupMessage(poweruptype='health')) + cmsg('Heald all') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage( + bs.PowerupMessage(poweruptype='health')) + cmsg('Heald '+is_name) + + elif m in [px+'curse', px+'cur']: + if n == []: + cmsg(f'Use: {px}curse all or {px}curse number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(bs.PowerupMessage(poweruptype='curse')) + cmsg('Cursed all') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage( + bs.PowerupMessage(poweruptype='curse')) + cmsg('Cursed '+is_name) + + elif m in [px+'sleep']: + if n == []: + cmsg(f'Use: {px}sleep all or {px}sleep number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage('knockout', 8000) + cmsg('Sleep all its Night :)') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage('knockout', 8000) + cmsg(is_name+' sleeped now') + + elif m in [px+'sp', px+'superpunch']: + if n == []: + cmsg(f'Use : {px}sp/superpunch all or {px}sp/superpunch number of list') + elif n[0] == 'all': + for i in activity_players: + if not i.actor._punch_power_scale == 15: + i.actor._punch_power_scale = 15 + i.actor._punch_cooldown = 0 + cmsg('Everyone enjoy your Super punches') + else: + i.actor._punch_power_scale = 1.2 + i.actor._punch_cooldown = 400 + cmsg('Super punches off now') + else: + try: + if not activity_players[int(n[0])].actor._punch_power_scale == 15: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor._punch_power_scale = 15 + activity_players[int(n[0])].actor._punch_cooldown = 0 + cmsg(is_name+' using super punches get away from him') + else: + activity_players[int(n[0])].actor._punch_power_scale = 1.2 + activity_players[int(n[0])].actor._punch_cooldown = 400 + cmsg(':( ') + except: + pass + + elif m in [px+'gloves', px+'punch']: + if n == []: + cmsg(f'Use: {px}gloves all or {px}gloves number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(bs.PowerupMessage(poweruptype='punch')) + cmsg('Free Gloves enjoy all') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage( + bs.PowerupMessage(poweruptype='punch')) + cmsg(is_name+' using gloves') + + elif m in [px+'shield', px+'protect']: + if n == []: + cmsg(f'Use: {px}shield all or {px}shield number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(bs.PowerupMessage(poweruptype='shield')) + cmsg('Everyone enjoy free shield :)') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage( + bs.PowerupMessage(poweruptype='shield')) + cmsg(is_name+' using shield') + + elif m in [px+'freeze', px+'ice']: + if n == []: + cmsg(f'Use: {px}freeze all or {px}freeze number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(bs.FreezeMessage()) + cmsg('Freezed all') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage(bs.FreezeMessage()) + cmsg('Un freezed '+is_name) + + elif m in [px+'unfreeze', px+'thaw']: + if n == []: + cmsg(f'Use: {px}unfreeze/thaw all or {px}unfreeze/thaw number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(bs.ThawMessage()) + cmsg('Un freezed all ') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage(bs.ThawMessage()) + cmsg('Un freezed '+is_name) + + elif m in [px+'fall']: + if n == []: + cmsg(f'Use: {px}fall all or {px}fall number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(bs.StandMessage()) + cmsg('Felt everyone') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage(bs.StandMessage()) + cmsg(is_name+' got felt') + + elif m in [px+'celebrate', px+'celeb']: + if n == []: + cmsg(f'Use: {px}celebrate all or {px}celebrate number of list') + elif n[0] == 'all': + for i in activity_players: + i.actor.node.handlemessage(bs.CelebrateMessage()) + cmsg('Celebrate all :)') + else: + is_name = session_players[int(n[0])].getname() + activity_players[int(n[0])].actor.node.handlemessage(bs.CelebrateMessage()) + cmsg(is_name+' is celebrating bt why?') + + elif m in [px+'fly']: + if n == []: + cmsg(f'Use: {px}fly all or {px}fly number of list') + elif n[0] == 'all': + for i in activity_players: + if not i.actor.node.fly == True: + i.actor.node.fly = True + cmsg('fly all dont go out ok') + else: + i.actor.node.fly = False + cmsg('flying mode off') + else: + try: + is_name = session_players[int(n[0])].getname() + if not activity_players[int(n[0])].actor.node.fly == True: + activity_players[int(n[0])].actor.node.fly = True + cmsg(is_name+' is flying') + else: + activity_players[int(n[0])].actor.node.fly = False + cmsg('fly off :(') + except: + cmsg('player not found') + pass + + elif m in [px+'gm', px+'godmode']: + if n == []: + cmsg(f'Use: {px}gm all or {px}gm number of list') + elif n[0] == 'all': + for i in activity_players: + if not i.actor.node.hockey == True: + i.actor.node.hockey = True + i.actor.node.invincible = True + i.actor._punch_power_scale = 7 + cmsg('Gmed all ') + else: + i.actor.node.hockey = False + i.actor.node.invincible = False + i.actor._punch_power_scale = 1.2 + cmsg('Un gmed all') + else: + try: + is_name = session_players[int(n[0])].getname() + if not activity_players[int(n[0])].actor.node.hockey == True: + activity_players[int(n[0])].actor.node.hockey = True + activity_players[int(n[0])].actor.node.invincible = True + activity_players[int(n[0])].actor._punch_power_scale = 7 + cmsg('Gmed '+is_name) + else: + activity_players[int(n[0])].actor.node.hockey = False + activity_players[int(n[0])].actor.node.invincible = False + activity_players[int(n[0])].actor._punch_power_scale = 1.2 + cmsg('un gmed '+is_name) + except: + cmsg('could not found player') + elif m in [px+'d_bomb', px+'default_bomb']: + if n == []: + cmsg( + f'Use: {px}d_bomb/default_bomb all or {px}d_bomb number of list ,type {px}d_bomb help for help') + elif n[0] == 'help': + cmsg("bombtypes - ['ice', 'impact', 'land_mine', 'normal', 'sticky','tnt']") + elif n[0] in ['ice', 'impact', 'land_mine', 'normal', 'sticky', 'tnt']: + for i in activity_players: + i.actor.bomb_type = n[0] + cmsg('default bomb type - '+str(n[0])+' now') + else: + cmsg('unkwon bombtype , type {px}d_bomb help for help') + + elif m in [px+'d_bomb_count', px+'default_bomb_count', px+'dbc']: + if n == []: + cmsg( + f'Use: {px}d_bomb_count/default_bomb/dbc all or {px}d_bomb_count/default_bomb_count/dbc number of list') + else: + try: + for i in activity_players: + i.actor.set_bomb_count(int(n[0])) + cmsg('default bomb count is '+(str(n[0]))+' now') + except: + cmsg('Must use number to use') + elif m in [px+'credits']: + if n == []: + cmsg(u'\U0001F95A created by Nazz \U0001F95A') + cmsg(u'\U0001F95A Nazz are past/present/future \U0001F95A') + cmsg(u'\U0001F95A everything is Nazz \U0001F95A') + + +class NewMainMenuWindow(mainmenu.MainMenuWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Display chat icon, but if user open/close gather it may disappear + bui.set_party_icon_always_visible(True) + +# bs.timer(0.05, _update, repeat=True) + + +def same(): + # bs.timer(0.5, _cmds._process_cmd, True) + _cmds._process_cmd() +# ba_meta export babase.Plugin + + +class _enableee(babase.Plugin): + timer = bs.AppTimer(0.5, same, repeat=True) + + def on_app_running(self): + mainmenu.MainMenuWindow = NewMainMenuWindow diff --git a/plugins/utilities/cheat_menu.py b/plugins/utilities/cheat_menu.py new file mode 100644 index 000000000..4e1b00369 --- /dev/null +++ b/plugins/utilities/cheat_menu.py @@ -0,0 +1,358 @@ +# Ported by brostos to api 8 +# Tool used to make porting easier.(https://github.com/bombsquad-community/baport) +"""CheatMenu | now cheat much as you can haha!! please eric sir dont kill me + + +Credits To: +Pranav"Modder"= Creator of the mod. +Emily"Skin and Modder"= Code and Mod Ideas.(pls dont be angry) :"( +And Me(Edited): Well Litterally Nothing lol. + +Important Note From the Creator: "I apreciate any kind of modification. So feel free to use or edit code or change credit string.... no problem. +this mod uses activity loop cheacks if change in config and update it on our player node" + +Really Awesome servers: + Bombsquad Consultancy Service - https://discord.gg/2RKd9QQdQY. + bombspot - https://discord.gg/ucyaesh. + cyclones - https://discord.gg/pJXxkbQ7kH. +""" +from __future__ import annotations + +__author__ = 'egg' +__version__ = 1.0 + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase + +from baenv import TARGET_BALLISTICA_BUILD as build_number +from bauiv1lib.settings.allsettings import AllSettingsWindow +from bascenev1lib.actor.spaz import Spaz + +from typing import ( + Text, + Tuple, + Optional, + Union, + get_args +) +type + +# Default Confings/Settings +CONFIG = "CheatMenu" +APPCONFIG = babase.app.config +Configs = { + "Unlimited Heath": False, + "SpeedBoots": False, + "Fly": False, + "SuperPunch": False, + "ImpactOnly": False, + "StickyOnly": False, + "IceOnly": False, + "Infinite Bombs": False, + "More Are Coming": False, + "Credits": False, +} + + +def setconfigs() -> None: + """Set required defualt configs for mod""" + if CONFIG not in APPCONFIG: + APPCONFIG[str(CONFIG)] = Configs + + for c in Configs: + if c not in APPCONFIG[str(CONFIG)]: + APPCONFIG[str(CONFIG)][str(c)] = Configs[str(c)] + else: + pass + APPCONFIG.apply_and_commit() + + +def update_config(config: str, change: any): + """update's given value in json config file of pluguin""" + APPCONFIG[str(CONFIG)][str(config)] = change + APPCONFIG.apply_and_commit() + + +# ba_meta require api 8 +# ba_meta export plugin +class Plugin(babase.Plugin): + def on_app_running(self) -> None: + if babase.app.build_number if build_number < 21282 else babase.app.env.build_number: + setconfigs() + self.overwrite() + + else: + babase.screenmessage(f'{__name__} only works on api 8') + + def overwrite(self) -> None: + AllSettingsWindow.init = AllSettingsWindow.__init__ + AllSettingsWindow.__init__ = AllSettingsWindowInit + + +# creating Cheat button, start button +def AllSettingsWindowInit(self, transition: str = 'in_right', origin_widget: bui.Widget = None): + self.init(transition) + + uiscale = bui.app.ui_v1.uiscale + btn_width = 720 if uiscale is babase.UIScale.SMALL else 400 + btn_height = 380 + + self.cheat_menu_btn = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(btn_width, btn_height), + size=(105, 50), + icon=bui.gettexture('settingsIcon'), + label='Cheats', + button_type='square', + text_scale=1.2, + on_activate_call=babase.Call( + on_cheat_menu_btn_press, self)) + + +# on cheat button press call Window +def on_cheat_menu_btn_press(self): + bui.containerwidget(edit=self._root_widget, + transition='out_scale') + bui.app.ui_v1.set_main_menu_window( + CheatMenuWindow( + transition='in_right').get_root_widget(), from_window=self._root_widget) + + +class CheatMenuWindow(bui.Window): + def __init__(self, + transition: Optional[str] = 'in_right') -> None: + + # background window, main widget parameters + uiscale = bui.app.ui_v1.uiscale + self._width = 870.0 if uiscale is babase.UIScale.SMALL else 670.0 + self._height = (390.0 if uiscale is babase.UIScale.SMALL else + 450.0 if uiscale is babase.UIScale.MEDIUM else 520.0) + extra_x = 100 if uiscale is babase.UIScale.SMALL else 0 + self.extra_x = extra_x + top_extra = 20 if uiscale is babase.UIScale.SMALL else 0 + + # scroll widget parameters + self._scroll_width = self._width - (100 + 2 * extra_x) + self._scroll_height = self._height - 115.0 + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 640.0 + self._spacing = 32 + self._extra_button_spacing = self._spacing * 2.5 + + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=(2.06 if uiscale is babase.UIScale.SMALL else + 1.4 if uiscale is babase.UIScale.MEDIUM else 1.0))) + + # back button widget + self._back_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(52 + self.extra_x, + self._height - 60 - top_extra), + size=(60, 60), + scale=0.8, + label=babase.charstr(babase.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self._back) + bui.containerwidget(edit=self._root_widget, + cancel_button=self._back_button) + + # window title, apears in top center of window + self._title_text = bui.textwidget( + parent=self._root_widget, + position=(0, self._height - 40 - top_extra), + size=(self._width, 25), + text='Cheat Menu', + color=bui.app.ui_v1.title_color, + scale=1.2, + h_align='center', + v_align='top') + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=(50 + extra_x, 50 - top_extra), + simple_culling_v=20.0, + highlight=False, + size=(self._scroll_width, + self._scroll_height), + selection_loops_to_parent=True) + bui.widget(edit=self._scrollwidget, + right_widget=self._scrollwidget) + + # subcontainer represents scroll widget and used as parent + self._subcontainer = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False, + selection_loops_to_parent=True) + + v = self._sub_height - 35 + v -= self._spacing * 1.2 + conf = APPCONFIG[str(CONFIG)] + + for checkbox in Configs: + bui.checkboxwidget( + parent=self._subcontainer, + autoselect=True, + position=(25.0, v), + size=(40, 40), + text=checkbox, + textcolor=(0.8, 0.8, 0.8), + value=APPCONFIG[CONFIG][checkbox], + on_value_change_call=babase.Call( + self.update, checkbox), + scale=1.4, + maxwidth=430) + v -= 70 + + def update(self, config: str, change) -> None: + """Change config and get our sounds + + Args: + config: str + change: any + """ + try: + if change == True and config == "Fly": + bui.screenmessage("Some maps may not work good for flying", + color=(1, 0, 0)) + update_config(config, change) + bui.getsound('gunCocking').play() + except Exception: + bui.screenmessage("error", color=(1, 0, 0)) + bui.getsound('error').play() + + try: + if change == True and config == "SuperPunch": + bui.screenmessage("SuperPunch Activated", + color=(1, 0, 0)) + elif change == False and config == "SuperPunch": + bui.screenmessage("Super Punch Deactivated", + color=(0.5, 0, 0)) + update_config(config, change) + bui.getsound('gunCocking').play() + except Exception: + bui.screenmessage("error", color=(1, 0, 0)) + bui.getsound('spazOw').play() + + try: + if change == True and config == "IceOnly": + bui.screenmessage("Ice Bombs Activated", + color=(0.1, 1, 1)) + elif change == False and config == "IceOnly": + bui.screenmessage("Ice Bombs Deactivated", + color=(1, 0, 0)) + update_config(config, change) + bui.getsound('gunCocking').play() + except Exception: + bui.screenmessage("error", color=(1, 0, 0)) + bui.getsound('spazOw').play() + try: + if change == True and config == "StickyOnly": + bui.screenmessage("Sticky Bombs Activated", + color=(0, 1, 0)) + elif change == False and config == "StickyOnly": + bui.screenmessage("Sticky Bombs Deactivated", + color=(1, 0, 0)) + update_config(config, change) + bui.getsound('gunCocking').play() + except Exception: + bui.screenmessage("error", color=(1, 0, 0)) + bui.getsound('spazOw').play() + + try: + if change == True and config == "ImpactOnly": + bui.screenmessage("Impact Bombs Activated", + color=(0.5, 0.5, 0.5)) + elif change == False and config == "ImpactOnly": + bui.screenmessage("Impact Bombs Deactivated", + color=(1, 0, 0)) + update_config(config, change) + bui.getsound('gunCocking').play() + except Exception: + bui.screenmessage("error", color=(1, 0, 0)) + bui.getsound('spazOw').play() + + try: + if change == True and config == "More Are Coming": + bui.screenmessage("Check out https://discord.gg/2RKd9QQdQY For More Mods", + color=(4, 9, 2)) + update_config(config, change) + bui.getsound('gunCocking').play() + except Exception: + bui.screenmessage("error", color=(1, 0, 0)) + bui.getsound('cheer').play() + + try: + if change == True and config == "Credits": + bui.screenmessage("To Pranav Made The Mod and Emily For Ideas, Thx", + color=(4, 9, 2)) + update_config(config, change) + bui.getsound('gunCocking').play() + except Exception: + bui.screenmessage("error", color=(1, 0, 0)) + bui.getsound('cheer').play() + + def _back(self) -> None: + """Kill the window and get back to previous one + """ + bui.containerwidget(edit=self._root_widget, + transition='out_scale') + bui.app.ui_v1.set_main_menu_window( + AllSettingsWindow( + transition='in_left').get_root_widget(), from_window=self._root_widget) + + +def ishost(): + session = bs.get_foreground_host_session() + with session.context: + for player in session.sessionplayers: + if player.inputdevice.client_id == -1: + return True + + +def activity_loop(): + if bs.get_foreground_host_activity() is not None: + activity = bs.get_foreground_host_activity() + with activity.context: + for player in activity.players: + if not ishost() or not player.actor: + return + config = APPCONFIG[CONFIG] + + player.actor.node.invincible = config["Unlimited Heath"] + player.actor.node.fly = config["Fly"] + player.actor.node.hockey = config["SpeedBoots"] + + if config["SuperPunch"]: + player.actor._punch_power_scale = 2 + + elif not config["SuperPunch"]: + player.actor._punch_power_scale = 1.2 + + if config["IceOnly"]: + player.actor.bomb_type = 'ice' + elif not config["IceOnly"]: + player.actor.bomb_type = 'normal' + player.actor.bomb_count = 1 + + if config["ImpactOnly"]: + player.actor.bomb_type = 'impact' + player.actor.bomb_count = 1 + + if config["StickyOnly"]: + player.actor.bomb_type = 'sticky' + player.actor.bomb_count = 1 + + if config["Infinite Bombs"]: + player.actor.bomb_count = 100 + + +timer = babase.AppTimer(2, activity_loop, repeat=True) diff --git a/plugins/utilities/colored_bomb_explosion_patches.py b/plugins/utilities/colored_bomb_explosion_patches.py new file mode 100644 index 000000000..a10764b37 --- /dev/null +++ b/plugins/utilities/colored_bomb_explosion_patches.py @@ -0,0 +1,56 @@ +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +import random +from bascenev1lib.actor import bomb + +if TYPE_CHECKING: + from typing import Sequence + + +class NewBlast(bomb.Blast): + def __init__( + self, + *, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + blast_radius: float = 2.0, + blast_type: str = 'normal', + source_player: bs.Player | None = None, + hit_type: str = 'explosion', + hit_subtype: str = 'normal', + ): + super().__init__( + position=position, + velocity=velocity, + blast_radius=blast_radius, + blast_type=blast_type, + source_player=source_player, + hit_type=hit_type, + hit_subtype=hit_subtype + ) + scorch_radius = self.radius + if self.blast_type == 'tnt': + scorch_radius *= 1.15 + scorch = bs.newnode( + 'scorch', + attrs={ + 'position': position, + 'size': scorch_radius * 0.5, + 'big': (self.blast_type == 'tnt'), + }, + ) + random_color = (random.random(), random.random(), random.random()) + scorch.color = babase.safecolor(random_color) + bs.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) + bs.timer(13.0, scorch.delete) + + +# ba_meta export babase.Plugin +class RandomColorsPlugin(babase.Plugin): + bomb.Blast = NewBlast diff --git a/plugins/utilities/colorscheme.py b/plugins/utilities/colorscheme.py index b339d045d..569a2582c 100644 --- a/plugins/utilities/colorscheme.py +++ b/plugins/utilities/colorscheme.py @@ -6,21 +6,21 @@ # Settings -> Advanced -> Enter Code # to bring up the colorscheme UI. -# ba_meta require api 7 -import _ba -import ba +# ba_meta require api 9 +import _babase +import babase +import bauiv1 as bui -from bastd.ui.colorpicker import ColorPicker +from bauiv1lib.colorpicker import ColorPicker -original_buttonwidget = ba.buttonwidget -original_containerwidget = ba.containerwidget -original_checkboxwidget = ba.checkboxwidget - -original_add_transaction = _ba.add_transaction +original_buttonwidget = bui.buttonwidget +original_containerwidget = bui.containerwidget +original_checkboxwidget = bui.checkboxwidget # We set this later so we store the overridden method in case the # player is using pro-unlocker plugins that override the -# `ba.app.accounts.have_pro` method. +# `bui.app.classic.accounts.have_pro` method. original_have_pro = None +original_add_transaction = bui.app.plus.add_v1_account_transaction class ColorScheme: @@ -43,24 +43,24 @@ class ColorScheme: -------- + Apply dark colorscheme: - >>> import _ba - >>> dark = _ba.ColorScheme((0.2,0.2,0.2), (0.8,0.8,0.8)) + >>> import _babase + >>> dark = _babase.ColorScheme((0.2,0.2,0.2), (0.8,0.8,0.8)) >>> dark.apply() # Reset back to game's default colorscheme >>> dark.disable() + Colorscheme that modifies only the main colors: - >>> import _ba - >>> bluey = _ba.ColorScheme(color=(0.1,0.3,0.6)) + >>> import _babase + >>> bluey = _babase.ColorScheme(color=(0.1,0.3,0.6)) >>> bluey.apply() # Reset back to game's default colorscheme >>> bluey.disable() + Colorscheme that modifies only the highlight colors: - >>> import _ba - >>> reddish = _ba.ColorScheme(highlight=(0.8,0.35,0.35)) + >>> import _babase + >>> reddish = _babase.ColorScheme(highlight=(0.8,0.35,0.35)) >>> reddish.apply() # Reset back to game's default colorscheme >>> reddish.disable() @@ -68,8 +68,8 @@ class ColorScheme: + Revert back to game's default colorscheme irrespective of whatever colorscheme is active at the moment: - >>> import _ba - >>> _ba.ColorScheme.disable() + >>> import _babase + >>> _babase.ColorScheme.disable() """ def __init__(self, color=None, highlight=None): @@ -94,13 +94,13 @@ def _custom_checkboxwidget(self, *args, **kwargs): def _apply_color(self): if self.color is None: raise TypeError("Expected color to be an (R,G,B) tuple.") - ba.containerwidget = self._custom_containerwidget + bui.containerwidget = self._custom_containerwidget def _apply_highlight(self): if self.highlight is None: raise TypeError("Expected highlight to be an (R,G,B) tuple.") - ba.buttonwidget = self._custom_buttonwidget - ba.checkboxwidget = self._custom_checkboxwidget + bui.buttonwidget = self._custom_buttonwidget + bui.checkboxwidget = self._custom_checkboxwidget def apply(self): if self.color: @@ -110,12 +110,12 @@ def apply(self): @staticmethod def _disable_color(): - ba.buttonwidget = original_buttonwidget - ba.checkboxwidget = original_checkboxwidget + bui.buttonwidget = original_buttonwidget + bui.checkboxwidget = original_checkboxwidget @staticmethod def _disable_highlight(): - ba.containerwidget = original_containerwidget + bui.containerwidget = original_containerwidget @classmethod def disable(cls): @@ -123,10 +123,10 @@ def disable(cls): cls._disable_highlight() -class ColorSchemeWindow(ba.Window): +class ColorSchemeWindow(bui.Window): def __init__(self, default_colors=((0.41, 0.39, 0.5), (0.5, 0.7, 0.25))): self._default_colors = default_colors - self._color, self._highlight = ba.app.config.get("ColorScheme", (None, None)) + self._color, self._highlight = babase.app.config.get("ColorScheme", (None, None)) self._last_color = self._color self._last_highlight = self._highlight @@ -139,87 +139,88 @@ def __init__(self, default_colors=((0.41, 0.39, 0.5), (0.5, 0.7, 0.25))): # A hack to let players select any RGB color value through the UI, # otherwise this is limited only to pro accounts. - ba.app.accounts_v1.have_pro = lambda: True + bui.app.classic.accounts.have_pro = lambda: True self.draw_ui() def draw_ui(self): - # Most of the stuff here for drawing the UI is referred from the - # game's bastd/ui/profile/edit.py, and so there could be some - # cruft here due to my oversight. - uiscale = ba.app.ui.uiscale - self._width = width = 480.0 if uiscale is ba.UIScale.SMALL else 380.0 - self._x_inset = x_inset = 40.0 if uiscale is ba.UIScale.SMALL else 0.0 + # NOTE: Most of the stuff here for drawing the UI is referred from the + # legacy (1.6 < version <= 1.7.19) game's bastd/ui/profile/edit.py, and + # so there could be some cruft here due to my oversight. + uiscale = bui.app.ui_v1.uiscale + self._width = width = 480.0 if uiscale is babase.UIScale.SMALL else 380.0 + self._x_inset = x_inset = 40.0 if uiscale is babase.UIScale.SMALL else 0.0 self._height = height = ( 275.0 - if uiscale is ba.UIScale.SMALL + if uiscale is babase.UIScale.SMALL else 288.0 - if uiscale is ba.UIScale.MEDIUM + if uiscale is babase.UIScale.MEDIUM else 300.0 ) spacing = 40 self._base_scale = ( 2.05 - if uiscale is ba.UIScale.SMALL + if uiscale is babase.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is babase.UIScale.MEDIUM else 1.0 ) - top_extra = 15 if uiscale is ba.UIScale.SMALL else 15 + top_extra = 15 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height + top_extra), + on_outside_click_call=self.cancel_on_outside_click, transition="in_right", scale=self._base_scale, - stack_offset=(0, 15) if uiscale is ba.UIScale.SMALL else (0, 0), + stack_offset=(0, 15) if uiscale is babase.UIScale.SMALL else (0, 0), ) ) - cancel_button = ba.buttonwidget( + cancel_button = bui.buttonwidget( parent=self._root_widget, position=(52 + x_inset, height - 60), size=(155, 60), scale=0.8, autoselect=True, - label=ba.Lstr(resource="cancelText"), + label=babase.Lstr(resource="cancelText"), on_activate_call=self._cancel, ) - ba.containerwidget(edit=self._root_widget, cancel_button=cancel_button) + bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button) - save_button = ba.buttonwidget( + save_button = bui.buttonwidget( parent=self._root_widget, position=(width - (177 + x_inset), height - 110), size=(155, 60), autoselect=True, scale=0.8, - label=ba.Lstr(resource="saveText"), + label=babase.Lstr(resource="saveText"), ) - ba.widget(edit=save_button, left_widget=cancel_button) - ba.buttonwidget(edit=save_button, on_activate_call=self.save) - ba.widget(edit=cancel_button, right_widget=save_button) - ba.containerwidget(edit=self._root_widget, start_button=save_button) + bui.widget(edit=save_button, left_widget=cancel_button) + bui.buttonwidget(edit=save_button, on_activate_call=self.save) + bui.widget(edit=cancel_button, right_widget=save_button) + bui.containerwidget(edit=self._root_widget, start_button=save_button) - reset_button = ba.buttonwidget( + reset_button = bui.buttonwidget( parent=self._root_widget, position=(width - (177 + x_inset), height - 60), size=(155, 60), color=(0.2, 0.5, 0.6), autoselect=True, scale=0.8, - label=ba.Lstr(resource="settingsWindowAdvanced.resetText"), + label=babase.Lstr(resource="settingsWindowAdvanced.resetText"), ) - ba.widget(edit=reset_button, left_widget=reset_button) - ba.buttonwidget(edit=reset_button, on_activate_call=self.reset) - ba.widget(edit=cancel_button, right_widget=reset_button) - ba.containerwidget(edit=self._root_widget, start_button=reset_button) + bui.widget(edit=reset_button, left_widget=reset_button) + bui.buttonwidget(edit=reset_button, on_activate_call=self.reset) + bui.widget(edit=cancel_button, right_widget=reset_button) + bui.containerwidget(edit=self._root_widget, start_button=reset_button) v = height - 65.0 v -= spacing * 3.0 b_size = 80 b_offs = 75 - self._color_button = ba.buttonwidget( + self._color_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width * 0.5 - b_offs - b_size * 0.5, v - 50), @@ -228,23 +229,23 @@ def draw_ui(self): label="", button_type="square", ) - ba.buttonwidget( - edit=self._color_button, on_activate_call=ba.Call(self._pick_color, "color") + bui.buttonwidget( + edit=self._color_button, on_activate_call=babase.Call(self._pick_color, "color") ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, h_align="center", v_align="center", position=(self._width * 0.5 - b_offs, v - 65), size=(0, 0), draw_controller=self._color_button, - text=ba.Lstr(resource="editProfileWindow.colorText"), + text=babase.Lstr(resource="editProfileWindow.colorText"), scale=0.7, - color=ba.app.ui.title_color, + color=bui.app.ui_v1.title_color, maxwidth=120, ) - self._highlight_button = ba.buttonwidget( + self._highlight_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width * 0.5 + b_offs - b_size * 0.5, v - 50), @@ -254,20 +255,20 @@ def draw_ui(self): button_type="square", ) - ba.buttonwidget( + bui.buttonwidget( edit=self._highlight_button, - on_activate_call=ba.Call(self._pick_color, "highlight"), + on_activate_call=babase.Call(self._pick_color, "highlight"), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, h_align="center", v_align="center", position=(self._width * 0.5 + b_offs, v - 65), size=(0, 0), draw_controller=self._highlight_button, - text=ba.Lstr(resource="editProfileWindow.highlightText"), + text=babase.Lstr(resource="editProfileWindow.highlightText"), scale=0.7, - color=ba.app.ui.title_color, + color=bui.app.ui_v1.title_color, maxwidth=120, ) @@ -286,49 +287,53 @@ def _pick_color(self, tag): tag=tag, ) + def cancel_on_outside_click(self): + bui.getsound("swish").play() + self._cancel() + def _cancel(self): if self._last_color and self._last_highlight: colorscheme = ColorScheme(self._last_color, self._last_highlight) colorscheme.apply() # Good idea to revert this back now so we do not break anything else. - ba.app.accounts_v1.have_pro = original_have_pro - ba.containerwidget(edit=self._root_widget, transition="out_right") + bui.app.classic.accounts.have_pro = original_have_pro + bui.containerwidget(edit=self._root_widget, transition="out_right") def reset(self, transition_out=True): if transition_out: - ba.playsound(ba.getsound("gunCocking")) - ba.app.config["ColorScheme"] = (None, None) + bui.getsound("gunCocking").play() + babase.app.config["ColorScheme"] = (None, None) # Good idea to revert this back now so we do not break anything else. - ba.app.accounts_v1.have_pro = original_have_pro - ba.app.config.commit() - ba.containerwidget(edit=self._root_widget, transition="out_right") + bui.app.classic.accounts.have_pro = original_have_pro + babase.app.config.commit() + bui.containerwidget(edit=self._root_widget, transition="out_right") def save(self, transition_out=True): if transition_out: - ba.playsound(ba.getsound("gunCocking")) + bui.getsound("gunCocking").play() colorscheme = ColorScheme( self._color or self._default_colors[0], self._highlight or self._default_colors[1], ) colorscheme.apply() # Good idea to revert this back now so we do not break anything else. - ba.app.accounts_v1.have_pro = original_have_pro - ba.app.config["ColorScheme"] = ( + bui.app.classic.accounts.have_pro = original_have_pro + babase.app.config["ColorScheme"] = ( self._color or self._default_colors[0], self._highlight or self._default_colors[1], ) - ba.app.config.commit() - ba.containerwidget(edit=self._root_widget, transition="out_right") + babase.app.config.commit() + bui.containerwidget(edit=self._root_widget, transition="out_right") def _set_color(self, color): self._color = color if self._color_button: - ba.buttonwidget(edit=self._color_button, color=color) + bui.buttonwidget(edit=self._color_button, color=color) def _set_highlight(self, color): self._highlight = color if self._highlight_button: - ba.buttonwidget(edit=self._highlight_button, color=color) + bui.buttonwidget(edit=self._highlight_button, color=color) def color_picker_selected_color(self, picker, color): # The `ColorPicker` calls this method in the delegate once a color @@ -364,7 +369,7 @@ def add(self, transaction_code, transaction_fn): self.custom_transactions[transaction_code] = transaction_fn def enable(self): - _ba.add_transaction = self._handle + bui.app.plus.add_v1_account_transaction = self._handle def launch_colorscheme_selection_window(): @@ -381,7 +386,7 @@ def launch_colorscheme_selection_window(): # has pro-unlocked or not if our plugin runs before the dedicated # pro-unlocker plugin has been applied. global original_have_pro - original_have_pro = ba.app.accounts_v1.have_pro + original_have_pro = bui.app.classic.accounts.have_pro ColorSchemeWindow() @@ -391,7 +396,7 @@ def colorscheme_transaction(transaction, *args, **kwargs): def load_colorscheme(): - color, highlight = ba.app.config.get("ColorScheme", (None, None)) + color, highlight = babase.app.config.get("ColorScheme", (None, None)) if color and highlight: colorscheme = ColorScheme(color, highlight) colorscheme.apply() @@ -400,7 +405,7 @@ def load_colorscheme(): def load_plugin(): # Allow access to changing colorschemes manually through the in-game # console. - _ba.ColorScheme = ColorScheme + _babase.ColorScheme = ColorScheme # Adds a new advanced code entry named "colorscheme" which can be # entered through Settings -> Advanced -> Enter Code, allowing # colorscheme modification through a friendly UI. @@ -411,10 +416,13 @@ def load_plugin(): load_colorscheme() -# ba_meta export plugin -class Main(ba.Plugin): +# ba_meta export babase.Plugin +class Main(babase.Plugin): def on_app_running(self): load_plugin() - def on_plugin_manager_prompt(self): + def has_settings_ui(self): + return True + + def show_settings_ui(self, source_widget): launch_colorscheme_selection_window() diff --git a/plugins/utilities/custom_death.py b/plugins/utilities/custom_death.py new file mode 100644 index 000000000..6762d94e1 --- /dev/null +++ b/plugins/utilities/custom_death.py @@ -0,0 +1,42 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 9 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from bascenev1lib.actor.spaz import Spaz + +if TYPE_CHECKING: + from typing import Any + + +Spaz.oldhandlemessage = Spaz.handlemessage + + +def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.color_texture = bs.gettexture('bonesColor') + self.node.color_mask_texture = bs.gettexture('bonesColorMask') + self.node.head_mesh = bs.getmesh('bonesHead') + self.node.torso_mesh = bs.getmesh('bonesTorso') + self.node.pelvis_mesh = bs.getmesh('bonesPelvis') + self.node.upper_arm_mesh = bs.getmesh('bonesUpperArm') + self.node.forearm_mesh = bs.getmesh('bonesForeArm') + self.node.hand_mesh = bs.getmesh('bonesHand') + self.node.upper_leg_mesh = bs.getmesh('bonesUpperLeg') + self.node.lower_leg_mesh = bs.getmesh('bonesLowerLeg') + self.node.toes_mesh = bs.getmesh('bonesToes') + self.node.style = 'bones' + self.oldhandlemessage(msg) + else: + return self.oldhandlemessage(msg) + + +# ba_meta export babase.Plugin +class CustomDeath(babase.Plugin): + Spaz.handlemessage = handlemessage diff --git a/plugins/utilities/disable_friendly_fire.py b/plugins/utilities/disable_friendly_fire.py new file mode 100644 index 000000000..6b9cf9c81 --- /dev/null +++ b/plugins/utilities/disable_friendly_fire.py @@ -0,0 +1,110 @@ +# ba_meta require api 9 +from __future__ import annotations +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +import bascenev1lib +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + pass + + +class BombPickupMessage: + """ message says that someone pick up the dropped bomb """ + + +# for bs.FreezeMessage +freeze: bool = True + +# ba_meta export babase.Plugin + + +class Plugin(babase.Plugin): + + # there are two ways to ignore our team player hits + # either change playerspaz handlemessage or change spaz handlemessage + def playerspaz_new_handlemessage(func: fuction) -> fuction: + def wrapper(*args, **kwargs): + global freeze + + # only run if session is dual team + if isinstance(args[0].activity.session, bs.DualTeamSession): + # when spaz got hurt by any reason this statement is runs. + if isinstance(args[1], bs.HitMessage): + our_team_players: list[type(args[0]._player)] + + # source_player + attacker = args[1].get_source_player(type(args[0]._player)) + + # our team payers + our_team_players = args[0]._player.team.players.copy() + + if len(our_team_players) > 0: + + # removing our self + our_team_players.remove(args[0]._player) + + # if we honding teammate or if we have a shield, do hit. + for player in our_team_players: + if player.actor.exists() and args[0]._player.actor.exists(): + if args[0]._player.actor.node.hold_node == player.actor.node or args[0]._player.actor.shield: + our_team_players.remove(player) + break + + if attacker in our_team_players: + freeze = False + return None + else: + freeze = True + + # if ice_bomb blast hits any spaz this statement runs. + elif isinstance(args[1], bs.FreezeMessage): + if not freeze: + freeze = True # use it and reset it + return None + + # orignal unchanged code goes here + func(*args, **kwargs) + + return wrapper + + # replace original fuction to modified function + bascenev1lib.actor.playerspaz.PlayerSpaz.handlemessage = playerspaz_new_handlemessage( + bascenev1lib.actor.playerspaz.PlayerSpaz.handlemessage) + + # let's add a message when bomb is pick by player + def bombfact_new_init(func: function) -> function: + def wrapper(*args): + + func(*args) # original code + + args[0].bomb_material.add_actions( + conditions=('they_have_material', SharedObjects.get().pickup_material), + actions=('message', 'our_node', 'at_connect', BombPickupMessage()), + ) + return wrapper + + # you get the idea + bascenev1lib.actor.bomb.BombFactory.__init__ = bombfact_new_init( + bascenev1lib.actor.bomb.BombFactory.__init__) + + def bomb_new_handlemessage(func: function) -> function: + def wrapper(*args, **kwargs): + # only run if session is dual team + if isinstance(args[0].activity.session, bs.DualTeamSession): + if isinstance(args[1], BombPickupMessage): + # get the pickuper and assign the pickuper to the source_player(attacker) of bomb blast + for player in args[0].activity.players: + if player.actor.exists(): + if player.actor.node.hold_node == args[0].node: + args[0]._source_player = player + break + + func(*args, **kwargs) # original + + return wrapper + + bascenev1lib.actor.bomb.Bomb.handlemessage = bomb_new_handlemessage( + bascenev1lib.actor.bomb.Bomb.handlemessage) diff --git a/plugins/utilities/disco_light.py b/plugins/utilities/disco_light.py new file mode 100644 index 000000000..ea66a8f72 --- /dev/null +++ b/plugins/utilities/disco_light.py @@ -0,0 +1,214 @@ +"""Disco Light Mod: V2.1 +Made by Cross Joy""" + +# If anyone who wanna help me on giving suggestion/ fix bugs/ creating PR, +# Can visit my github https://github.com/CrossJoy/Bombsquad-Modding + +# You can contact me through discord: +# My Discord Id: Cross Joy#0721 +# My BS Discord Server: https://discord.gg/JyBY6haARJ + + +# ---------------------------------------------------------------------------- +# Add disco light into the game, so you can +# play with your friends under the colorful light. :) + +# type '/disco' in your chat box to activate or +# type '/disco off' to deactivate the disco light. + + +# Coop and multiplayer compatible. +# Work on any 1.7.37+ ver. + +# Note: +# The plugin commands only works on the host with the plugin activated. +# Other clients/players can't use the commands. + +# v2.1 +# - Enhance compatibility with other mods +# - Tint change when /disco off will be more dynamic. + +# ---------------------------------------------------------------------------- + +# ba_meta require api 9 + + +from __future__ import annotations + +import bascenev1 as bs +import babase +from bascenev1 import _gameutils +import random + +from bascenev1 import animate + + +class DiscoLight: + + def __init__(self): + activity = bs.get_foreground_host_activity() + self.globalnodes = activity.globalsnode.tint + + # Activate disco light. + def start(self): + activity = bs.get_foreground_host_activity() + with activity.context: + self.partyLight(True) + self.rainbow(activity) + + # Deactivate disco light. + def stop(self): + activity = bs.get_foreground_host_activity() + with activity.context: + self.partyLight(False) + self.stop_rainbow(activity) + + # Create and animate colorful spotlight. + def partyLight(self, switch=True): + from bascenev1._nodeactor import NodeActor + x_spread = 10 + y_spread = 5 + positions = [[-x_spread, -y_spread], [0, -y_spread], [0, y_spread], + [x_spread, -y_spread], [x_spread, y_spread], + [-x_spread, y_spread]] + times = [0, 2700, 1000, 1800, 500, 1400] + + # Store this on the current activity, so we only have one at a time. + activity = bs.getactivity() + activity.camera_flash_data = [] # type: ignore + for i in range(6): + r = random.choice([0.5, 1]) + g = random.choice([0.5, 1]) + b = random.choice([0.5, 1]) + light = NodeActor( + bs.newnode('light', + attrs={ + 'position': ( + positions[i][0], 0, positions[i][1]), + 'radius': 1.0, + 'lights_volumes': False, + 'height_attenuated': False, + 'color': (r, g, b) + })) + sval = 1.87 + iscale = 1.3 + tcombine = bs.newnode('combine', + owner=light.node, + attrs={ + 'size': 3, + 'input0': positions[i][0], + 'input1': 0, + 'input2': positions[i][1] + }) + assert light.node + tcombine.connectattr('output', light.node, 'position') + xval = positions[i][0] + yval = positions[i][1] + spd = 1.0 + random.random() + spd2 = 1.0 + random.random() + animate(tcombine, + 'input0', { + 0.0: xval + 0, + 0.069 * spd: xval + 10.0, + 0.143 * spd: xval - 10.0, + 0.201 * spd: xval + 0 + }, + loop=True) + animate(tcombine, + 'input2', { + 0.0: yval + 0, + 0.15 * spd2: yval + 10.0, + 0.287 * spd2: yval - 10.0, + 0.398 * spd2: yval + 0 + }, + loop=True) + animate(light.node, + 'intensity', { + 0.0: 0, + 0.02 * sval: 0, + 0.05 * sval: 0.8 * iscale, + 0.08 * sval: 0, + 0.1 * sval: 0 + }, + loop=True, + offset=times[i]) + if not switch: + bs.timer(0.1, + light.node.delete) + activity.camera_flash_data.append(light) # type: ignore + + # Create RGB tint. + def rainbow(self, activity) -> None: + """Create RGB tint.""" + + cnode = bs.newnode('combine', + attrs={ + 'input0': self.globalnodes[0], + 'input1': self.globalnodes[1], + 'input2': self.globalnodes[2], + 'size': 3 + }) + + _gameutils.animate(cnode, 'input0', + {0.0: 1.0, 1.0: 1.0, 2.0: 1.0, 3.0: 1.0, + 4.0: 0.2, 5.0: 0.1, 6.0: 0.5, + 7.0: 1.0}, loop=True) + + _gameutils.animate(cnode, 'input1', + {0.0: 0.2, 1.0: 0.2, 2.0: 0.5, 3.0: 1.0, + 4.0: 1.0, 5.0: 0.1, 6.0: 0.3, + 7.0: 0.2}, loop=True) + + _gameutils.animate(cnode, 'input2', + {0.0: 0.2, 1.0: 0.2, 2.0: 0.0, 3.0: 0.0, + 4.0: 0.2, 5.0: 1.0, 6.0: 1.0, + 7.0: 0.2}, loop=True) + + cnode.connectattr('output', activity.globalsnode, 'tint') + + # Revert to the original map tint. + def stop_rainbow(self, activity): + """Revert to the original map tint.""" + c_existing = activity.globalsnode.tint + # map_name = activity.map.getname() + tint = self.globalnodes + + cnode = bs.newnode('combine', + attrs={ + 'input0': c_existing[0], + 'input1': c_existing[1], + 'input2': c_existing[2], + 'size': 3 + }) + + _gameutils.animate(cnode, 'input0', {0: c_existing[0], 1.0: tint[0]}) + _gameutils.animate(cnode, 'input1', {0: c_existing[1], 1.0: tint[1]}) + _gameutils.animate(cnode, 'input2', {0: c_existing[2], 1.0: tint[2]}) + + cnode.connectattr('output', activity.globalsnode, 'tint') + + +# New chat func to add some commands to activate/deactivate the disco light. +def new_chat_message(func): + def wrapper(*args, **kwargs): + func(*args, **kwargs) + activity = bs.get_foreground_host_activity() + with activity.context: + try: + if not activity.disco_light: + activity.disco_light = DiscoLight() + except: + activity.disco_light = DiscoLight() + if args[0] == '/disco': + activity.disco_light.start() + elif args[0] == '/disco off': + activity.disco_light.stop() + + return wrapper + + +# ba_meta export babase.Plugin +class ByCrossJoy(babase.Plugin): + def __init__(self): + # Replace new chat func to the original game codes. + bs.chatmessage = new_chat_message(bs.chatmessage) diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py new file mode 100644 index 000000000..94a09a505 --- /dev/null +++ b/plugins/utilities/discord_richpresence.py @@ -0,0 +1,1253 @@ +# Released under the MIT and Apache License. See LICENSE for details. +# +"""placeholder :clown:""" + +# ba_meta require api 9 +#!"Made to you by @brostos & @Dliwk" +# TODO +# - Update to the latest libs + + +from __future__ import annotations +from urllib.request import Request, urlopen, urlretrieve +from pathlib import Path +from os import getcwd, remove +from os.path import abspath +from bauiv1lib.popup import PopupWindow + +import asyncio +import sys +import http.client +import ast +import uuid +import json +import socket +import time +import threading +import shutil +import hashlib +import logging +import babase +import _babase +import bascenev1 as bs +import bascenev1lib +import bauiv1 as bui +from baenv import TARGET_BALLISTICA_BUILD as build_number + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Tuple + + +MAPNAME_ID = { + "bombsquadicon": "963448129900908595", + "zigzagpreview": "963448133130522624", + "tiptoppreview": "963448133168279582", + "towerdpreview": "963448135886200912", + "thepadpreview": "963448137916248084", + "steprightuppreview": "963448141728862248", + "roundaboutpreview": "963448143997972550", + "rampagepreview": "963448146422296676", + "monkeyfacepreview": "963448151182831626", + "footballstadiumpreview": "963448158719983646", + "doomshroompreview": "963448160993292368", + "cragcastlepreview": "963448163048513536", + "courtyardpreview": "963448166127120504", + "bridgitpreview": "963448169180565654", + "biggpreview": "963448172905127996", + "alwayslandpreview": "963448174163423252", + "bigg": "1013013392455376977", + "bridgit": "1013013400139333632", + "courtyard": "1013013410776096788", + "cragcastle": "1013013423132528700", + "doomshroom": "1013013438223622224", + "footballstadium": "1013013452517810226", + "hockeystadium": "1013013464060547112", + "monkeyface": "1013013477721383023", + "rampage": "1013013484830728273", + "roundabout": "1013013508323037264", + "steprightup": "1013013567768907826", + "thepad": "1013013577197699163", + "tiptop": "1013013593089904721", + "towerd": "1013013604531970131", + "zigzag": "1013013618188619816", + "bombsquadlogo2": "1013016083701190726", + "windows": "1084050785488338984", + "linux": "1084078945944739920", + "lobby": "1084180821973418226", + "ranking": "1084224689272004719", + "rampagelevelcolor": "1086989941541703741", + "landmine": "1087000404866371766", + "rgbstripes": "1087000416492990474", + "shrapnel1color": "1087151233225195590", + "bonescolor": "1087151164077899928", + "bridgitlevelcolor": "1087151178674094182", + "crossout": "1087151197963681902", + "naturebackgroundcolor": "1087151209896476782", + "zigzaglevelcolor": "1087151253206876241", + "zoeicon": "1087151266989363240", + "bg": "1087564057890000906", + "alwayslandlevelcolor": "1087564765406167080", + "hockeystadiumpreview": "1087574349285961768", + "mac": "1087584375287336992", + "flyer": "1087584543147561051", + "replay": "1087592122393301102", + "coop": "1097697042891018311", + "ffa": "1097697050214269008", + "lobbysmall": "1097697055926923335", + "replaysmall": "1097697062746853386", + "teams": "1097697068727935036", + "bacongreece": "1097700754623565894", + "basketballstadium": "1097700771501441167", + "flapland": "1097700783622979664", + "alwaysland": "1097700794213613610", + "hoveringwood": "1097700802321199224", + "jrmponslaught": "1097700810479124520", + "jrmprunaround": "1097700817194205286", + "lakefrigid": "1097700828023898203", + "mushfeud": "1097700836920000594", + "pillar_bases": "1097700846340407427", + "powerup_factory": "1097700854422851656", + "snowballpit": "1097700869673341009", + "stoneishfort": "1097700887826272308", + "toiletdonut": "1097700898584666193", + "whereeaglesdare": "1097700904972587109", + "android": "1097728392280932453", +} +ANDROID = babase.app.classic.platform == "android" +APP_VERSION = ( + _babase.app.version + if build_number < 21282 + else ( + _babase.app.env.engine_version + if build_number > 21823 + else _babase.app.env.version + ) +) + + +if ANDROID: # !can add ios in future + + # Installing websocket + def get_module(): + install_path = Path( + abspath(bs.app.env.python_directory_app) + ) # For the guys like me on windows + path = Path(f"{install_path}/websocket.tar.gz") + file_path = Path(f"{install_path}/websocket") + source_dir = Path(f"{install_path}/websocket-client-1.6.1/websocket") + if not Path(f"{file_path}/__init__.py").exists(): # YouKnowDev + url = "https://files.pythonhosted.org/packages/b1/34/3a5cae1e07d9566ad073fa6d169bf22c03a3ba7b31b3c3422ec88d039108/websocket-client-1.6.1.tar.gz" + try: + # fix issue where the file delete themselves + try: + shutil.rmtree(file_path) + except: + pass + filename, headers = urlretrieve(url, filename=path) + with open(filename, "rb") as f: + content = f.read() + assert ( + hashlib.md5(content).hexdigest() + == "86bc69b61947943627afc1b351c0b5db" + ) + shutil.unpack_archive(filename, install_path, format="gztar") + remove(path) + shutil.copytree(source_dir, file_path) + shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) + except Exception as e: + if type(e) == shutil.Error: + shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) + else: + pass + + get_module() + + from websocket import WebSocketConnectionClosedException + import websocket + + start_time = time.time() + + class PresenceUpdate: + def __init__(self): + self.ws = websocket.WebSocketApp( + "wss://gateway.discord.gg/?encoding=json&v=10", + on_open=self.on_open, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close, + ) + self.heartbeat_interval = int(41250) + self.resume_gateway_url: str | None = None + self.session_id: str | None = None + self.stop_heartbeat_thread = threading.Event() + self.do_once: bool = True + self.state: str | None = "In Game" + self.details: str | None = "Main Menu" + self.start_timestamp = time.time() + self.large_image_key: str | None = "bombsquadicon" + self.large_image_text: str | None = "BombSquad Icon" + self.small_image_key: str | None = None + self.small_image_text: str | None = ( + f"{_babase.app.classic.platform.capitalize()}({APP_VERSION})" + ) + self.media_proxy = "mp:/app-assets/963434684669382696/{}.png" + self.identify: bool = False + self.party_id: str = str(uuid.uuid4()) + self.party_size = 1 + self.party_max = 8 + + def presence(self): + largetxt = MAPNAME_ID[self.large_image_key] + smalltxt = MAPNAME_ID[self.small_image_key] + + presencepayload = { + "op": 3, + "d": { + "since": start_time, # Fixed the unlimited time bug + "status": "online", + "afk": "false", + "activities": [ + { + "name": "BombSquad", + "type": 0, + "application_id": "963434684669382696", + "state": self.state, + "details": self.details, + # "timestamps": {"start": start_time}, + "party": { + "id": self.party_id, + "size": [self.party_size, self.party_max], + }, + "assets": { + "large_image": self.media_proxy.format(largetxt), + "large_text": self.large_image_text, + "small_image": self.media_proxy.format(smalltxt), + "small_text": self.small_image_text, + }, + "client_info": { + "version": 0, + "os": "android", + "client": "mobile", + }, + "buttons": ["Discord Server", "Download BombSquad"], + "metadata": { + "button_urls": [ + "https://discord.gg/bombsquad-ballistica-official-1001896771347304639", + "https://bombsquad-community.web.app/download", + ] + }, + } + ], + }, + } + try: + self.ws.send(json.dumps(presencepayload)) + except WebSocketConnectionClosedException: + pass + + def on_message(self, ws, message): + message = json.loads(message) + try: + self.heartbeat_interval = message["d"]["heartbeat_interval"] + except: + pass + try: + self.resume_gateway_url = message["d"]["resume_gateway_url"] + self.session_id = message["d"]["session_id"] + except: + pass + + def on_error(self, ws, error): + logging.exception(error) + + def on_close(self, ws, close_status_code, close_msg): + ( + print("Closed Discord Connection Successfully") + if close_status_code == 1000 + else print( + f"Closed Discord Connection with code {close_status_code} and message {close_msg}" + ) + ) + + def on_open(self, ws): + print("Connected to Discord Websocket") + + def heartbeats(): + """Sending heartbeats to keep the connection alive""" + if self.do_once: + heartbeat_payload = { + "op": 1, + "d": 251, + } # step two keeping connection alive by sending heart beats and receiving opcode 11 + self.ws.send(json.dumps(heartbeat_payload)) + self.do_once = False + + def identify(): + """Identifying to the gateway and enable by using user token and the intents we will be using e.g 256->For Presence""" + token = self.brosCrypt("None") + identify_payload = { + "op": 2, + "d": { + "token": token, + "properties": { + "os": "linux", + "browser": "Discord Android", + "device": "android", + }, + "intents": 256, + }, + } # step 3 send an identify + self.ws.send(json.dumps(identify_payload)) + + identify() + while True: + heartbeat_payload = {"op": 1, "d": self.heartbeat_interval} + + try: + self.ws.send(json.dumps(heartbeat_payload)) + time.sleep(self.heartbeat_interval / 1000) + except: + pass + + if self.stop_heartbeat_thread.is_set(): + self.stop_heartbeat_thread.clear() + break + + threading.Thread(target=heartbeats, daemon=True, name="heartbeat").start() + + def start(self): + if ( + Path( + f"{_babase.app.env.python_directory_user}/__pycache__/token.txt" + ).exists() + or Path(f"{getcwd()}/token.txt").exists() + ): + try: + with open(f"{getcwd()}/token.txt", "r") as f: + token = bytes.fromhex(f.read()).decode("utf-8") + except FileNotFoundError: + with open( + f"{_babase.app.env.python_directory_user}/__pycache__/token.txt", + "r", + ) as f: + token = bytes.fromhex(f.read()).decode("utf-8") + + self.brosCrypt(token) + try: + remove( + Path( + f"{_babase.app.env.python_directory_user}/__pycache__/token.txt" + ) + ) + except FileNotFoundError: + try: + remove(Path(f"{getcwd()}/token.txt")) + except FileNotFoundError: + pass + try: + self.brosCrypt(babase.app.config.get("token")) + except: + pass + try: + del babase.app.config["token"] + except KeyError: + pass + + if babase.app.config.get("encrypted_tokey"): + try: + while True: + urlopen("http://www.google.com", timeout=5) + threading.Thread( + target=self.ws.run_forever, daemon=True, name="websocket" + ).start() + return + except Exception: + return + + def close(self): + self.stop_heartbeat_thread.set() + self.do_once = True + self.ws.close() + + @staticmethod + def brosCrypt(naked_token): + + import babase + import bauiv1 as bui + + import random + import string + import textwrap + + random_string = naked_token + plus = bui.app.plus + pb_id = plus.get_v1_account_misc_read_val_2( + "resolvedAccountID", None + ).strip("pb-==") + + # Initialize the characters + chars = " " + string.punctuation + string.digits + string.ascii_letters + chars = list(chars) + + # Function to encrypt or decrypt text with a given key + def process_text(text, key, mode="encrypt"): + result_text = "" + for letter in text: + if mode == "encrypt": + index = chars.index(letter) + result_text += key[index] + elif mode == "decrypt": + index = key.index(letter) + result_text += chars[index] + else: + raise ValueError("Mode must be 'encrypt' or 'decrypt'") + return result_text + + def encrypt(): + # Generate the random string and split it into parts + part_length = len(random_string) // len(pb_id) + parts = textwrap.wrap(random_string, part_length) + + # Encrypt each part with its own unique key + encrypted_parts = {} + keys = {} + + for i, part in enumerate(parts): + key = chars.copy() + random.shuffle(key) + encrypted_part = process_text(part, key, mode="encrypt") + encrypted_parts[pb_id[i]] = encrypted_part + keys[pb_id[i]] = "".join(key) + + # Concatenate all keys with (==) separator + master_key = "(==)".join(keys.values()) + + encrypted_key = { + "encrypted_parts": encrypted_parts, + "master_key": master_key, + } + babase.app.config["encrypted_tokey"] = encrypted_key + babase.app.config.commit() + + def decrypt(): + # Split the master key to get individual key + master_key = babase.app.config.get("encrypted_tokey")["master_key"] + encrypted_parts = babase.app.config.get("encrypted_tokey")[ + "encrypted_parts" + ] + split_keys_list = master_key.split("(==)") + + # Create a dictionary to map pb_id letters to repective keys + keys_dict = dict(zip(pb_id, split_keys_list)) + + # Decrypt each part using the keys from the JSON object + decrypted_parts = {} + for char in pb_id: # Process in pb_id order + if char in encrypted_parts and char in keys_dict: + decrypted_parts[char] = process_text( + encrypted_parts[char], + keys_dict[char], + mode="decrypt" + ) + + token = "" + # Print the decrypted parts to verify + for letter, part in decrypted_parts.items(): + # print(f"{letter}: {part}") + token += part + + return token + + if naked_token == 'None': + return decrypt() + else: + encrypt() + + +if not ANDROID: + # installing pypresence + def get_module(): + + install_path = Path(abspath(bs.app.env.python_directory_app)) + path = Path(f"{install_path}/pypresence.tar.gz") + file_path = Path(f"{install_path}/pypresence") + source_dir = Path(f"{install_path}/pypresence-4.3.0/pypresence") + if not file_path.exists(): + url = "https://files.pythonhosted.org/packages/f4/2e/d110f862720b5e3ba1b0b719657385fc4151929befa2c6981f48360aa480/pypresence-4.3.0.tar.gz" + try: + filename, headers = urlretrieve(url, filename=path) + with open(filename, "rb") as f: + content = f.read() + assert ( + hashlib.md5(content).hexdigest() + == "f7c163cdd001af2456c09e241b90bad7" + ) + shutil.unpack_archive(filename, install_path, format="gztar") + shutil.copytree(source_dir, file_path) + shutil.rmtree(Path(f"{install_path}/pypresence-4.3.0")) + remove(path) + except: + pass + + get_module() + + from pypresence import PipeClosed, DiscordError, DiscordNotFound + from pypresence.utils import get_event_loop + import pypresence + + DEBUG = True + + def print_error(err: str, include_exception: bool = False) -> None: + if DEBUG: + if include_exception: + logging.exception(err) + else: + logging.error(err) + else: + print(f"ERROR in discordrp.py: {err}") + + def log(msg: str) -> None: + if DEBUG: + print(f"LOG in discordrp.py: {msg}") + + def _run_overrides() -> None: + old_init = bs.Activity.__init__ + + def new_init(self, *args: Any, **kwargs: Any) -> None: # type: ignore + old_init(self, *args, **kwargs) + self._discordrp_start_time = time.mktime(time.localtime()) + + bs.Activity.__init__ = new_init # type: ignore + + old_connect = bs.connect_to_party + + def new_connect(*args, **kwargs) -> None: # type: ignore + global _last_server_addr + global _last_server_port + old_connect(*args, **kwargs) + _last_server_addr = kwargs.get("address") or args[0] + # ! Joining a game on same device as host NB check what happens if host is port forwarded you join it and check joining a server port forwarded or not + _last_server_port = ( + kwargs.get("port") or args[1] if len(args) > 1 else 43210 + ) + + bs.connect_to_party = new_connect + + start_time = time.time() + + class RpcThread(threading.Thread): + def __init__(self): + super().__init__(name="RpcThread") + self.rpc = pypresence.Presence(963434684669382696) + self.state: str | None = "In Game" + self.details: str | None = "Main Menu" + self.start_timestamp = time.mktime(time.localtime()) + self.large_image_key: str | None = "bombsquadicon" + self.large_image_text: str | None = "BombSquad Icon" + self.small_image_key: str | None = None + self.small_image_text: str | None = None + self.party_id: str = str(uuid.uuid4()) + self.party_size = 1 + self.party_max = 8 + self.join_secret: str | None = None + self._last_update_time: float = 0 + self._last_secret_update_time: float = 0 + self._last_connect_time: float = 0 + self.should_close = False + self.connection_to_host_info = None + + @staticmethod + def is_discord_running(): + for i in range(6463, 6473): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.01) + try: + conn = s.connect_ex(("localhost", i)) + s.close() + if conn == 0: + s.close() + return True + except: + s.close() + return False + + def _generate_join_secret(self): + # resp = requests.get('https://legacy.ballistica.net/bsAccessCheck').text + try: + connection_info = self.connection_to_host_info + if connection_info: + addr = _last_server_addr + port = _last_server_port + else: + with urlopen("https://legacy.ballistica.net/bsAccessCheck") as resp: + resp = resp.read().decode() + resp = ast.literal_eval(resp) + addr = resp["address"] + port = resp["port"] + addr, port = addr, port + secret_dict = { + "format_version": 1, + "hostname": addr, + "port": port, + } + + self.join_secret = json.dumps(secret_dict) + except Exception as _: + pass + + def _update_secret(self): + #! use in game thread + threading.Thread(target=self._generate_join_secret, daemon=True).start() + self._last_secret_update_time = time.time() + + def run(self) -> None: + if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + asyncio.set_event_loop(get_event_loop()) + + while not self.should_close: + if time.time() - self._last_update_time > 0.1: + self._do_update_presence() + if time.time() - self._last_secret_update_time > 15: + self._update_secret() + time.sleep(0.03) + + def _subscribe(self, event: str, **args): + self.rpc.send_data( + 1, + { + "nonce": f"{time.time():.20f}", + "cmd": "SUBSCRIBE", + "evt": event, + "args": args, + }, + ) + data = self.rpc.loop.run_until_complete(self.rpc.read_output()) + self.handle_event(data) + + def _subscribe_events(self): + self._subscribe("ACTIVITY_JOIN") + self._subscribe("ACTIVITY_JOIN_REQUEST") + + # def _update_presence(self) -> None: + # self._last_update_time = time.time() + # try: + # self._do_update_presence() + # except (AttributeError, AssertionError): + # try: + # self._reconnect() + # except Exception: + # print_error("failed to update presence", include_exception= True) + + def _reconnect(self) -> None: + self.rpc.connect() + self._subscribe_events() + self._do_update_presence() + self._last_connect_time = time.time() + + def _do_update_presence(self) -> None: + if RpcThread.is_discord_running(): + self._last_update_time = time.time() + try: + data = self.rpc.update( + state=self.state or " ", + details=self.details, + start=start_time, + large_image=self.large_image_key, + large_text=self.large_image_text, + small_image=self.small_image_key, + small_text=self.small_image_text, + party_id=self.party_id, + party_size=[self.party_size, self.party_max], + join=self.join_secret, + ) + self.handle_event(data) + except (PipeClosed, DiscordError, AssertionError, AttributeError): + try: + self._reconnect() + except (DiscordNotFound, DiscordError): + pass + + def handle_event(self, data): + evt = data["evt"] + if evt is None: + return + + data = data.get("data", {}) + + if evt == "ACTIVITY_JOIN": + secret = data.get("secret") + try: + server = json.loads(secret) + format_version = server["format_version"] + except Exception: + logging.exception("discordrp: unknown activity join format") + else: + try: + if format_version == 1: + hostname = server["hostname"] + port = server["port"] + self._connect_to_party(hostname, port) + except Exception: + logging.exception( + f"discordrp: incorrect activity join data, {format_version=}" + ) + + elif evt == "ACTIVITY_JOIN_REQUEST": + user = data.get("user", {}) + uid = user.get("id") + username = user.get("username") + avatar = user.get("avatar") + self.on_join_request(username, uid, avatar) + + def _connect_to_party(self, hostname, port) -> None: + babase.pushcall( + babase.Call(bs.connect_to_party, hostname, port), from_other_thread=True + ) + + def on_join_request(self, username, uid, avatar) -> None: + del uid # unused + del avatar # unused + babase.pushcall( + babase.Call( + bui.screenmessage, + "Discord: {} wants to join!".format(username), + color=(0.0, 1.0, 0.0), + ), + from_other_thread=True, + ) + #! check this function for sound creation error + babase.pushcall( + lambda: bui.getsound("bellMed").play(), from_other_thread=True + ) + + +class Discordlogin(PopupWindow): + + def __init__(self): + # pylint: disable=too-many-locals + _uiscale = bui.app.ui_v1.uiscale + self._transitioning_out = False + s = ( + 1.25 + if _uiscale is babase.UIScale.SMALL + else 1.27 if _uiscale is babase.UIScale.MEDIUM else 1.3 + ) + self._width = 380 * s + self._height = 150 + 150 * s + bg_color = (0.5, 0.4, 0.6) + log_btn_colour = ( + (0.10, 0.95, 0.10) + if not babase.app.config.get("encrypted_tokey") + else (1.00, 0.15, 0.15) + ) + log_txt = ( + "LOG IN" if not babase.app.config.get("encrypted_tokey") else "LOG OUT" + ) + self.code = False + self.resp = "Placeholder" + # Plucked from https://gist.github.com/brostosjoined/3bb7b96c1f6397d389427f46e104005f + import base64 + + X_Super_Properties = { + "os": "Android", + "browser": "Android Chrome", + # ! Find the devices original (Linux; Android 10; K) + "browser_user_agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Mobile Safari/537.36", + } + self.headers = { + "User-Agent": X_Super_Properties["browser_user_agent"], + "Content-Type": "application/json", + "X-Super-Properties": base64.b64encode( + json.dumps(X_Super_Properties).encode() + ).decode(), + } + + # creates our _root_widget + PopupWindow.__init__( + self, + position=(0.0, 0.0), + size=(self._width, self._height), + scale=( + 2.1 + if _uiscale is babase.UIScale.SMALL + else 1.5 if _uiscale is babase.UIScale.MEDIUM else 1.0 + ), + bg_color=bg_color, + ) + + self._cancel_button = bui.buttonwidget( + parent=self.root_widget, + position=(25, self._height - 40), + size=(50, 50), + scale=0.58, + label="", + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=bui.gettexture("crossOut"), + iconscale=1.2, + ) + + bui.imagewidget( + parent=self.root_widget, + position=(180, self._height - 55), + size=(32 * s, 32 * s), + texture=bui.gettexture("discordLogo"), + color=(10 - 0.32, 10 - 0.39, 10 - 0.96), + ) + + self.email_widget = bui.textwidget( + parent=self.root_widget, + text="Email/Phone Number", + size=(400, 70), + position=(50, 180), + h_align="left", + v_align="center", + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220, + ) + + self.password_widget = bui.textwidget( + parent=self.root_widget, + text="Password", + size=(400, 70), + position=(50, 120), + h_align="left", + v_align="center", + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220, + ) + + bui.containerwidget(edit=self.root_widget, cancel_button=self._cancel_button) + + bui.textwidget( + parent=self.root_widget, + position=(265, self._height - 37), + size=(0, 0), + h_align="center", + v_align="center", + scale=1.0, + text="Discord", + maxwidth=200, + color=(0.80, 0.80, 0.80), + ) + + bui.textwidget( + parent=self.root_widget, + position=(265, self._height - 78), + size=(0, 0), + h_align="center", + v_align="center", + scale=1.0, + text="??Use at your own risk??\n ??discord account might get terminated??", + maxwidth=200, + color=(1.00, 0.15, 0.15), + ) + + self._login_button = bui.buttonwidget( + parent=self.root_widget, + position=(120, 65), + size=(400, 80), + scale=0.58, + label=log_txt, + color=log_btn_colour, + on_activate_call=self.login, + autoselect=True, + ) + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + bui.containerwidget(edit=self.root_widget, transition="out_scale") + + def on_bascenev1libup_cancel(self) -> None: + bui.getsound("swish").play() + self._transition_out() + + def backup_2fa_code(self, ticket): + if babase.do_once(): + self.email_widget.delete() + self.password_widget.delete() + + self.backup_2fa_widget = bui.textwidget( + parent=self.root_widget, + text="2FA/Discord Backup code", + size=(400, 70), + position=(50, 120), + h_align="left", + v_align="center", + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220, + ) + + mfa_json = { + "code": bui.textwidget(query=self.backup_2fa_widget), + "ticket": ticket, + "login_source": None, + "gift_code_sku_id": None + } + code = mfa_json["code"] + if len(code) == 6 and code.isdigit(): # len the backup code and check if it number for 2fa + try: + payload_2FA = json.dumps(mfa_json, separators=(',', ':')) + conn_2FA = http.client.HTTPSConnection("discord.com") + conn_2FA.request( + "POST", "/api/v9/auth/mfa/totp", payload_2FA, self.headers + ) + res_2FA = conn_2FA.getresponse().read() + token = json.loads(res_2FA)["token"] + PresenceUpdate.brosCrypt(token) + bui.screenmessage("Successfully logged in", (0.21, 1.0, 0.20)) + bui.getsound("shieldUp").play() + self.on_bascenev1libup_cancel() + PresenceUpdate().start() + except: + self.code = True + bui.screenmessage("Incorrect code", (1.00, 0.15, 0.15)) + bui.getsound("error").play() + + def login(self): + if not babase.app.config.get("encrypted_tokey") and self.code == False: + try: + + login_json = { + "login": bui.textwidget(query=self.email_widget), + "password": bui.textwidget(query=self.password_widget), + "undelete": False, + "login_source": None, + "gift_code_sku_id": None, + } + + conn = http.client.HTTPSConnection("discord.com") + + login_payload = json.dumps(login_json, separators=(",", ":")) + conn.request("POST", "/api/v9/auth/login", login_payload, self.headers) + login_res = conn.getresponse().read() + + try: + token = json.loads(login_res)["token"] + PresenceUpdate.brosCrypt(token) + bui.screenmessage("Successfully logged in", (0.21, 1.0, 0.20)) + bui.getsound("shieldUp").play() + self.on_bascenev1libup_cancel() + PresenceUpdate().start() + except KeyError: + try: + ticket = json.loads(login_res)["ticket"] + bui.screenmessage( + "Input your 2FA or Discord Backup code", (0.21, 1.0, 0.20) + ) + bui.getsound("error").play() + self.resp = ticket + self.backup_2fa_code(ticket=ticket) + self.code = True + except KeyError: + bui.screenmessage("Incorrect credentials", (1.00, 0.15, 0.15)) + bui.getsound("error").play() + + except: + bui.screenmessage("Connect to the internet", (1.00, 0.15, 0.15)) + bui.getsound("error").play() + + conn.close() + elif self.code == True: + self.backup_2fa_code(ticket=self.resp) + + else: + self.email_widget.delete() + self.password_widget.delete() + del babase.app.config["encrypted_tokey"] + babase.app.config.commit() + bui.getsound("shieldDown").play() + bui.screenmessage("Account successfully removed!!", (0.10, 0.10, 1.00)) + self.on_bascenev1libup_cancel() + PresenceUpdate().close() + + +def get_class(): + if ANDROID: + return PresenceUpdate() + elif not ANDROID: + return RpcThread() + + +# ba_meta export babase.Plugin +class DiscordRP(babase.Plugin): + def __init__(self) -> None: + self.update_timer: bs.Timer | None = None + self.rpc_thread = get_class() + self._last_server_info: str | None = None + + if not ANDROID: + _run_overrides() + + def on_app_running(self) -> None: + if not ANDROID: + threading.Thread( + target=self.rpc_thread.start, daemon=True, name="start_rpc" + ).start() + + self.update_timer = bs.AppTimer( + 1, bs.WeakCall(self.update_status), repeat=True + ) + if ANDROID: + self.rpc_thread.start() + self.update_timer = bs.AppTimer( + 4, bs.WeakCall(self.update_status), repeat=True + ) + + def has_settings_ui(self): + if ANDROID: + return True + else: + return False + + def show_settings_ui(self, button): + Discordlogin() + + def on_app_shutdown(self) -> None: + if not ANDROID and self.rpc_thread.is_discord_running(): + self.rpc_thread.rpc.close() + self.rpc_thread.should_close = True + + def on_app_pause(self) -> None: + self.rpc_thread.close() + + def on_app_resume(self) -> None: + global start_time + start_time = time.time() + self.rpc_thread.start() + + def _get_current_activity_name(self) -> str | None: + act = bs.get_foreground_host_activity() + if isinstance(act, bs.GameActivity): + return act.name + + this = "Lobby" + name: str | None = ( + act.__class__.__name__.replace("Activity", "") + .replace("ScoreScreen", "Ranking") + .replace("Coop", "") + .replace("MultiTeam", "") + .replace("Victory", "") + .replace("EndSession", "") + .replace("Transition", "") + .replace("Draw", "") + .replace("FreeForAll", "") + .replace("Join", this) + .replace("Team", "") + .replace("Series", "") + .replace("CustomSession", "Custom Session(mod)") + ) + + if name == "MainMenu": + name = "Main Menu" + if name == this: + self.rpc_thread.large_image_key = "lobby" + self.rpc_thread.large_image_text = "Bombing up" + self.rpc_thread.small_image_key = "lobbysmall" + if name == "Ranking": + self.rpc_thread.large_image_key = "ranking" + self.rpc_thread.large_image_text = "Viewing Results" + return name + + def _get_current_map_name(self) -> Tuple[str | None, str | None]: + act = bs.get_foreground_host_activity() + if isinstance(act, bs.GameActivity): + texname = act.map.get_preview_texture_name() + if texname: + return act.map.name, texname.lower().removesuffix("preview") + return None, None + + def update_status(self) -> None: + roster = bs.get_game_roster() + try: + connection_info = ( + bs.get_connection_to_host_info() + if build_number < 21727 + else bs.get_connection_to_host_info_2() + ) + self.rpc_thread.connection_to_host_info = connection_info + except (RuntimeError, TypeError): + pass + + self.rpc_thread.large_image_key = "bombsquadicon" + self.rpc_thread.large_image_text = "BombSquad" + self.rpc_thread.small_image_key = _babase.app.classic.platform + self.rpc_thread.small_image_text = ( + f"{_babase.app.classic.platform.capitalize()}({APP_VERSION})" + ) + try: + if not ANDROID: + svinfo = str(connection_info) + if self._last_server_info != svinfo: + self._last_server_info = svinfo + self.rpc_thread.party_id = str(uuid.uuid4()) + self.rpc_thread._update_secret() + + if connection_info: + hostname = socket.gethostname() + local_ip = socket.gethostbyname(hostname) + + if bs.get_connection_to_host_info_2().address == local_ip: + self.rpc_thread.details = "Local Server" + else: + self.rpc_thread.details = "Online" + + servername = connection_info.name + self.rpc_thread.party_size = max( + 1, sum(len(client["players"]) for client in roster) + ) + self.rpc_thread.party_max = max(8, self.rpc_thread.party_size) + if len(servername) == 19 and "Private Party" in servername: + self.rpc_thread.state = "Private Party" + elif servername == "": # A local game joinable from the internet + try: + offlinename = json.loads( + bs.get_game_roster()[0]["spec_string"] + )["n"] + if len(offlinename) > 19: # Thanks Rikko + self.rpc_thread.state = offlinename[slice(19)] + "..." + else: + self.rpc_thread.state = offlinename + except IndexError: + pass + else: + if len(servername) > 19: + self.rpc_thread.state = servername[slice(19)] + ".." + else: + self.rpc_thread.state = servername[slice(19)] + + if not connection_info: + self.rpc_thread.details = ( + "Local" # ! replace with something like ballistica github cause + ) + self.rpc_thread.state = self._get_current_activity_name() + self.rpc_thread.party_size = max(1, len(roster)) + self.rpc_thread.party_max = max(1, bs.get_public_party_max_size()) + + if ( + bs.get_foreground_host_session() is not None + and self.rpc_thread.details == "Local" + ): + session = ( + bs.get_foreground_host_session() + .__class__.__name__.replace("MainMenuSession", "") + .replace("EndSession", "") + .replace("FreeForAllSession", ": FFA") + .replace("DualTeamSession", ": Teams") + .replace("CoopSession", ": Coop") + ) + if len(session) > 1: + self.rpc_thread.small_image_key = session.replace( + ": ", "" + ).lower() + self.rpc_thread.small_image_text = session.replace(": ", "") + self.rpc_thread.details = self.rpc_thread.details + if ( + self.rpc_thread.state == "NoneType" + ): # sometimes the game just breaks which means its not really watching replay FIXME + self.rpc_thread.state = "Watching Replay" + self.rpc_thread.large_image_key = "replay" + self.rpc_thread.large_image_text = "Viewing Awesomeness" + self.rpc_thread.small_image_key = "replaysmall" + except UnboundLocalError: + pass + + act = bs.get_foreground_host_activity() + session = bs.get_foreground_host_session() + if act: + from bascenev1lib.game.elimination import EliminationGame + from bascenev1lib.game.thelaststand import TheLastStandGame + from bascenev1lib.game.meteorshower import MeteorShowerGame + from bascenev1lib.game.football import FootballCoopGame + from bascenev1lib.game.easteregghunt import EasterEggHuntGame + + # noinspection PyUnresolvedReferences,PyProtectedMember + try: + self.rpc_thread.start_timestamp = act._discordrp_start_time # type: ignore + except AttributeError: + # This can be the case if plugin launched AFTER activity + # has been created; in that case let's assume it was + # created just now. + self.rpc_thread.start_timestamp = act._discordrp_start_time = time.mktime( # type: ignore + time.localtime() + ) + if isinstance(act, EliminationGame): + alive_count = len([p for p in act.players if p.lives > 0]) + self.rpc_thread.details += f" ({alive_count} players left)" + elif isinstance(act, TheLastStandGame): + # noinspection PyProtectedMember + points = act._score + self.rpc_thread.details += f" ({points} points)" + elif isinstance(act, MeteorShowerGame): + with act.context: + sec = bs.time() - act._timer.getstarttime() + secfmt = "" + if sec < 60: + secfmt = f"{sec:.2f}" + else: + secfmt = f"{int(sec) // 60:02}:{sec:.2f}" + self.rpc_thread.details += f" ({secfmt})" + # elif isinstance(act, OnslaughtGame): + # score = act._score + # level = act._wavenum + # # self. + elif isinstance(act, FootballCoopGame): + # try: + # score = f"{act.teams[0].score} : {act.teams[1].score}" + # except IndexError: + score = f"{act.teams[0].score} : {act._bot_team.score}" + self.rpc_thread.details = score + # elif isinstance(act, RunaroundGame) + # score = act._score + # level = act._wavenum + # lives = act._lives + elif isinstance(act, EasterEggHuntGame): + eggs_collected = len(act._eggs) - 1 + self.rpc_thread.details = f"{eggs_collected} eggs collected" + # elif isinstance(act, TargetPracticeGame): + # #for FFA + # scoere = bs.get_foreground_host_activity().players[0].score + + # if isinstance(session, ba.DualTeamSession): + # scores = ':'.join([ + # str(t.customdata['score']) + # for t in session.sessionteams + # ]) + # self.rpc_thread.details += f' ({scores})' + + mapname, short_map_name = self._get_current_map_name() + if mapname: + asset_keys = MAPNAME_ID.keys() + if short_map_name in asset_keys: + self.rpc_thread.large_image_text = mapname + self.rpc_thread.large_image_key = short_map_name + # self.rpc_thread.small_image_key = 'bombsquadlogo2' + # self.rpc_thread.small_image_text = 'BombSquad' + + if _babase.get_idle_time() / (1000 * 60) % 60 >= 0.4: + self.rpc_thread.details = f"AFK in {self.rpc_thread.details}" + if not ANDROID: + self.rpc_thread.large_image_key = ( + "https://media.tenor.com/uAqNn6fv7x4AAAAM/bombsquad-spaz.gif" + ) + if babase.app.config.get("encrypted_tokey") and ANDROID: + #! This function might cause some errors + try: + self.rpc_thread.presence() + except Exception as e: + # raise (e) + pass diff --git a/plugins/utilities/easy_connect.py b/plugins/utilities/easy_connect.py new file mode 100644 index 000000000..a3b88a7b1 --- /dev/null +++ b/plugins/utilities/easy_connect.py @@ -0,0 +1,736 @@ +# -*- coding: utf-8 -*- +# ba_meta require api 9 + +''' =========================================== + EasyConnect by Mr.Smoothy | + verion 1.7 | + https://discord.gg/ucyaesh | + Serverconnector X IPPORTRevealer | + for bombsquad v1.7.37+ | + ============================================== +''' + +# .................___________________________________________ +# WATCH IN ACTION https://www.youtube.com/watch?v=jwi2wKwZblQ +# .................___________________________________________ + +# Have any idea/suggestion/bug report > send message on discord mr.smoothy#5824 +# Download modshttps://bombsquad-community.web.app/mods +# Discord:- +# mr.smoothy#5824 + +# DONT EDIT ANYTHING WITHOUT PERMISSION + +# join Bombsquad Community Server - + +# https://discord.gg/ucyaesh + +# REQUIREMENTS +# built for bs 1.7.37 and above + +# by Mr.Smoothy for Bombsquad version 1.7.37+ + +import _babase +import babase +import bauiv1 as bui +import bascenev1 as bs +import threading +from bauiv1lib.gather import manualtab, publictab +from bauiv1lib import popup +from dataclasses import dataclass +import random +from enum import Enum +from bauiv1lib.popup import PopupMenuWindow +from typing import Any, Optional, Callable +from bauiv1lib.gather.publictab import PublicGatherTab +import json +import urllib.request +import time + + +ENABLE_SERVER_BANNING = True +DEBUG_SERVER_COMMUNICATION = False +DEBUG_PROCESSING = False +""" +This banned servers list is maintained by commmunity , its not official. +Reason for ban can be (not limited to) using abusive server names , using server name of a reputed server/community +without necessary permissions. +Report such case on community discord channels +https://discord.gg/ucyaesh +https://ballistica.net/discord +""" +BCSURL = 'https://bcs.ballistica.workers.dev/bannedservers' + + +def is_game_version_lower_than(version): + """ + Returns a boolean value indicating whether the current game + version is lower than the passed version. Useful for addressing + any breaking changes within game versions. + """ + game_version = tuple(map(int, babase.app.version.split("."))) + version = tuple(map(int, version.split("."))) + return game_version < version + + +def updateBannedServersCache(): + response = None + config = babase.app.config + if not isinstance(config.get('Banned Servers'), list): + config['Banned Servers'] = [] + try: + req = urllib.request.Request(BCSURL, headers={ + "User-Agent": f'BS{_babase.env().get("build_number", 0)}', "Accept-Language": "en-US,en;q=0.9", }) + response = urllib.request.urlopen(req).read() + data = json.loads(response.decode('utf-8')) + bannedlist = [] + for server in data["servers"]: + bannedlist.append(server["ip"]) + config['Banned Servers'] = bannedlist + config.commit() + print("updated cache") + except Exception as e: + print(e) + + +class _HostLookupThread(threading.Thread): + """Thread to fetch an addr.""" + + def __init__(self, name: str, port: int, + call: Callable[[Optional[str], int], Any]): + super().__init__() + self._name = name + self._port = port + self._call = call + + def run(self) -> None: + result: Optional[str] + try: + import socket + result = socket.gethostbyname(self._name) + except Exception: + result = None + babase.pushcall(lambda: self._call(result, self._port), + from_other_thread=True) + + +def newbuild_favorites_tab(self, region_width: float, region_height: float) -> None: + c_height = region_height - 20 + v = c_height - 35 - 25 - 30 + self.retry_inter = 0.0 + uiscale = bui.app.ui_v1.uiscale + self._width = region_width + x_inset = 100 if uiscale is babase.UIScale.SMALL else 0 + self._height = (578 if uiscale is babase.UIScale.SMALL else + 670 if uiscale is babase.UIScale.MEDIUM else 800) + + self._scroll_width = self._width - 130 + 2 * x_inset + self._scroll_height = self._height - 180 + x_inset = 100 if uiscale is babase.UIScale.SMALL else 0 + + c_height = self._scroll_height - 20 + sub_scroll_height = c_height - 63 + self._favorites_scroll_width = sub_scroll_width = ( + 680 if uiscale is babase.UIScale.SMALL else 640) + + v = c_height - 30 + + b_width = 140 if uiscale is babase.UIScale.SMALL else 178 + b_height = (90 if uiscale is babase.UIScale.SMALL else + 142 if uiscale is babase.UIScale.MEDIUM else 130) + b_space_extra = (0 if uiscale is babase.UIScale.SMALL else + -2 if uiscale is babase.UIScale.MEDIUM else -5) + + btnv = (c_height - (48 if uiscale is babase.UIScale.SMALL else + 45 if uiscale is babase.UIScale.MEDIUM else 40) - + b_height) + # ================= smoothy ============= + + bui.textwidget(parent=self._container, + position=(90 if uiscale is babase.UIScale.SMALL else 120, btnv + + 120 if uiscale is babase.UIScale.SMALL else btnv+90), + size=(0, 0), + h_align='center', + color=(0.8, 0.8, 0.8), + v_align='top', + text="Auto") + btnv += 50 if uiscale is babase.UIScale.SMALL else 0 + + bui.buttonwidget(parent=self._container, + size=(30, 30), + position=(25 if uiscale is babase.UIScale.SMALL else 40, + btnv+10), + + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self.auto_retry_dec, + text_scale=1.3 if uiscale is babase.UIScale.SMALL else 1.2, + label="-", + autoselect=True) + self.retry_inter_text = bui.textwidget(parent=self._container, + position=( + 90 if uiscale is babase.UIScale.SMALL else 120, btnv+25), + size=(0, 0), + h_align='center', + color=(0.8, 0.8, 0.8), + v_align='center', + text=str(self.retry_inter) if self.retry_inter > 0.0 else 'off') + bui.buttonwidget(parent=self._container, + size=(30, 30), + position=(125 if uiscale is babase.UIScale.SMALL else 155, + btnv+10), + + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self.auto_retry_inc, + text_scale=1.3 if uiscale is babase.UIScale.SMALL else 1.2, + label="+", + autoselect=True) + + btnv -= b_height + b_space_extra + + self._favorites_connect_button = btn1 = bui.buttonwidget( + parent=self._container, + size=(b_width, b_height), + position=(25 if uiscale is babase.UIScale.SMALL else 40, btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_favorites_connect_press, + text_scale=1.0 if uiscale is babase.UIScale.SMALL else 1.2, + label=babase.Lstr(resource='gatherWindow.manualConnectText'), + autoselect=True) + if uiscale is babase.UIScale.SMALL and bui.app.ui_v1.use_toolbars: + bui.widget(edit=btn1, + left_widget=bui.get_special_widget('back_button')) + btnv -= b_height + b_space_extra + bui.buttonwidget(parent=self._container, + size=(b_width, b_height), + position=(25 if uiscale is babase.UIScale.SMALL else 40, + btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_favorites_edit_press, + text_scale=1.0 if uiscale is babase.UIScale.SMALL else 1.2, + label=babase.Lstr(resource='editText'), + autoselect=True) + btnv -= b_height + b_space_extra + bui.buttonwidget(parent=self._container, + size=(b_width, b_height), + position=(25 if uiscale is babase.UIScale.SMALL else 40, + btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_favorite_delete_press, + text_scale=1.0 if uiscale is babase.UIScale.SMALL else 1.2, + label=babase.Lstr(resource='deleteText'), + autoselect=True) + + v -= sub_scroll_height + 23 + self._scrollwidget = scrlw = bui.scrollwidget( + parent=self._container, + position=(190 if uiscale is babase.UIScale.SMALL else 225, v), + size=(sub_scroll_width, sub_scroll_height), + claims_left_right=True) + bui.widget(edit=self._favorites_connect_button, + right_widget=self._scrollwidget) + self._columnwidget = bui.columnwidget(parent=scrlw, + left_border=10, + border=2, + margin=0, + claims_left_right=True) + self._no_parties_added_text = bui.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + text='', + color=(0.6, 0.6, 0.6), + scale=1.2, + position=( + ( + (240 if uiscale is bui.UIScale.SMALL else 225) + + sub_scroll_width * 0.5 + ), + v + sub_scroll_height * 0.5, + ), + glow_type='uniform', + ) + + self._favorite_selected = None + self._refresh_favorites() + + +def new_on_favorites_connect_press(self) -> None: + if self._favorite_selected is None: + self._no_favorite_selected_error() + + else: + config = babase.app.config['Saved Servers'][self._favorite_selected] + _HostLookupThread(name=config['addr'], + port=config['port'], + call=bs.WeakCall( + self._host_lookup_result)).start() + + if self.retry_inter > 0 and (bs.get_connection_to_host_info_2() == {} or bs.get_connection_to_host_info_2() == None or bs.get_connection_to_host_info_2()['build_number'] == 0): + bui.screenmessage("Server full or unreachable, Retrying....") + self._retry_timer = bs.AppTimer(self.retry_inter, babase.Call( + self._on_favorites_connect_press)) + + +def auto_retry_inc(self): + + self.retry_inter += 0.5 + bui.textwidget(edit=self.retry_inter_text, text='%.1f' % self.retry_inter) + + +def auto_retry_dec(self): + if self.retry_inter > 0.0: + self.retry_inter -= 0.5 + + if self.retry_inter == 0.0: + bui.textwidget(edit=self.retry_inter_text, text='off') + else: + bui.textwidget(edit=self.retry_inter_text, text='%.1f' % self.retry_inter) + + +@dataclass +class PartyEntry: + """Info about a public party.""" + address: str + index: int + queue: Optional[str] = None + port: int = -1 + name: str = '' + size: int = -1 + size_max: int = -1 + claimed: bool = False + ping: Optional[float] = None + ping_interval: float = -1.0 + next_ping_time: float = -1.0 + ping_attempts: int = 0 + ping_responses: int = 0 + stats_addr: Optional[str] = None + clean_display_index: Optional[int] = None + + def get_key(self) -> str: + """Return the key used to store this party.""" + return f'{self.address}_{self.port}' + + +class SelectionComponent(Enum): + """Describes what part of an entry is selected.""" + NAME = 'name' + STATS_BUTTON = 'stats_button' + + +@dataclass +class Selection: + """Describes the currently selected list element.""" + entry_key: str + component: SelectionComponent + + +def _clear(self) -> None: + for widget in [ + self._name_widget, self._size_widget, self._ping_widget, + self._stats_button + ]: + if widget: + + widget.delete() + + +def update(self, index: int, party: PartyEntry, sub_scroll_width: float, + sub_scroll_height: float, lineheight: float, + columnwidget: bui.Widget, join_text: bui.Widget, + filter_text: bui.Widget, existing_selection: Optional[Selection], + tab: PublicGatherTab) -> None: + """Update for the given data.""" + # pylint: disable=too-many-locals + + # Quick-out: if we've been marked clean for a certain index and + # we're still at that index, we're done. + plus = bui.app.plus + assert plus is not None + + # Quick-out: if we've been marked clean for a certain index and + # we're still at that index, we're done. + if party.clean_display_index == index: + return + + ping_good = plus.get_v1_account_misc_read_val('pingGood', 100) + ping_med = plus.get_v1_account_misc_read_val('pingMed', 500) + + self._clear() + hpos = 20 + vpos = sub_scroll_height - lineheight * index - 50 + self._name_widget = bui.textwidget( + text=bui.Lstr(value=party.name), + parent=columnwidget, + size=(sub_scroll_width * 0.63, 20), + position=(0 + hpos, 4 + vpos), + selectable=True, + on_select_call=bui.WeakCall( + tab.set_public_party_selection, + Selection(party.get_key(), SelectionComponent.NAME), + ), + on_activate_call=bui.WeakCall(tab.on_public_party_activate, party), + click_activate=True, + maxwidth=sub_scroll_width * 0.45, + corner_scale=1.4, + autoselect=True, + color=(1, 1, 1, 0.3 if party.ping is None else 1.0), + h_align='left', + v_align='center', + ) + bui.widget( + edit=self._name_widget, + left_widget=join_text, + show_buffer_top=64.0, + show_buffer_bottom=64.0, + ) + if existing_selection == Selection( + party.get_key(), SelectionComponent.NAME + ): + bui.containerwidget( + edit=columnwidget, selected_child=self._name_widget + ) + if party.stats_addr or True: + url = party.stats_addr.replace( + '${ACCOUNT}', + plus.get_v1_account_misc_read_val_2( + 'resolvedAccountID', 'UNKNOWN' + ), + ) + self._stats_button = bui.buttonwidget( + color=(0.3, 0.6, 0.94), + textcolor=(1.0, 1.0, 1.0), + label='....', + parent=columnwidget, + autoselect=True, + on_activate_call=bui.Call(bui.open_url, url), + on_select_call=bui.WeakCall( + tab.set_public_party_selection, + Selection(party.get_key(), SelectionComponent.STATS_BUTTON), + ), + size=(120, 40), + position=(sub_scroll_width * 0.66 + hpos, 1 + vpos), + scale=0.9, + ) + bui.buttonwidget(edit=self._stats_button, on_activate_call=bui.WeakCall( + self.on_stats_click, self._stats_button, party)) + if existing_selection == Selection( + party.get_key(), SelectionComponent.STATS_BUTTON + ): + bui.containerwidget( + edit=columnwidget, selected_child=self._stats_button + ) + + self._size_widget = bui.textwidget( + text=str(party.size) + '/' + str(party.size_max), + parent=columnwidget, + size=(0, 0), + position=(sub_scroll_width * 0.86 + hpos, 20 + vpos), + scale=0.7, + color=(0.8, 0.8, 0.8), + h_align='right', + v_align='center', + ) + + if index == 0: + bui.widget(edit=self._name_widget, up_widget=filter_text) + if self._stats_button: + bui.widget(edit=self._stats_button, up_widget=filter_text) + + self._ping_widget = bui.textwidget( + parent=columnwidget, + size=(0, 0), + position=(sub_scroll_width * 0.94 + hpos, 20 + vpos), + scale=0.7, + h_align='right', + v_align='center', + ) + if party.ping is None: + bui.textwidget( + edit=self._ping_widget, text='-', color=(0.5, 0.5, 0.5) + ) + else: + bui.textwidget( + edit=self._ping_widget, + text=str(int(party.ping)), + color=(0, 1, 0) + if party.ping <= ping_good + else (1, 1, 0) + if party.ping <= ping_med + else (1, 0, 0), + ) + + party.clean_display_index = index + + +def _get_popup_window_scale() -> float: + uiscale = bui.app.ui_v1.uiscale + return (2.3 if uiscale is babase.UIScale.SMALL else + 1.65 if uiscale is babase.UIScale.MEDIUM else 1.23) + + +_party = None + + +def on_stats_click(self, widget, party): + global _party + _party = party + choices = ['connect', 'copyqueue', "save"] + DisChoices = [babase.Lstr(resource="ipp", fallback_value="Connect by IP"), babase.Lstr( + resource="copy id", fallback_value="Copy Queue ID"), babase.Lstr(value="Save")] + if party.stats_addr: + choices.append('stats') + if 'discord' in party.stats_addr: + txt = "Discord" + elif 'yout' in party.stats_addr: + txt = "Youtube" + else: + txt = party.stats_addr[0:13] + DisChoices.append(babase.Lstr(value=txt)) + PopupMenuWindow( + position=widget.get_screen_space_center(), + scale=_get_popup_window_scale(), + choices=choices, + choices_display=DisChoices, + current_choice="stats", + delegate=self) + + +def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None: + pass + + +def popup_menu_selected_choice(self, window: popup.PopupMenu, + choice: str) -> None: + """Called when a menu entry is selected.""" + # Unused arg. + plus = babase.app.plus + if choice == 'stats': + url = _party.stats_addr.replace( + '${ACCOUNT}', + plus.get_v1_account_misc_read_val_2('resolvedAccountID', + 'UNKNOWN')) + bui.open_url(url) + elif choice == 'connect': + PartyQuickConnect(_party.address, _party.port) + elif choice == 'save': + config = babase.app.config + ip_add = _party.address + p_port = _party.port + title = _party.name + if not isinstance(config.get('Saved Servers'), dict): + config['Saved Servers'] = {} + config['Saved Servers'][f'{ip_add}@{p_port}'] = { + 'addr': ip_add, + 'port': p_port, + 'name': title + } + config.commit() + bui.screenmessage("Server saved to manual") + bui.getsound('gunCocking').play() + elif choice == "copyqueue": + babase.clipboard_set_text(_party.queue) + bui.getsound('gunCocking').play() + + +def _update_party_lists(self) -> None: + if not self._party_lists_dirty: + return + starttime = time.time() + config = babase.app.config + plus = babase.app.plus + bannedservers = config.get('Banned Servers', []) + assert len(self._parties_sorted) == len(self._parties) + + self._parties_sorted.sort( + key=lambda p: ( + p[1].ping if p[1].ping is not None else 999999.0, + p[1].index, + ) + ) + + # If signed out or errored, show no parties. + if ( + plus.get_v1_account_state() != 'signed_in' + or not self._have_valid_server_list + ): + self._parties_displayed = {} + else: + if self._filter_value: + filterval = self._filter_value.lower() + self._parties_displayed = { + k: v + for k, v in self._parties_sorted + if (filterval in v.name.lower() or filterval in v.address) and (v.address not in bannedservers if ENABLE_SERVER_BANNING else True) + } + else: + self._parties_displayed = { + k: v + for k, v in self._parties_sorted + if (v.address not in bannedservers if ENABLE_SERVER_BANNING else True) + } + + # Any time our selection disappears from the displayed list, go back to + # auto-selecting the top entry. + if ( + self._selection is not None + and self._selection.entry_key not in self._parties_displayed + ): + self._have_user_selected_row = False + + # Whenever the user hasn't selected something, keep the first visible + # row selected. + if not self._have_user_selected_row and self._parties_displayed: + firstpartykey = next(iter(self._parties_displayed)) + self._selection = Selection(firstpartykey, SelectionComponent.NAME) + + self._party_lists_dirty = False + if DEBUG_PROCESSING: + print( + f'Sorted {len(self._parties_sorted)} parties in' + f' {time.time()-starttime:.5f}s.' + ) + + +def replace(): + manualtab.ManualGatherTab._build_favorites_tab = newbuild_favorites_tab + manualtab.ManualGatherTab._on_favorites_connect_press = new_on_favorites_connect_press + manualtab.ManualGatherTab.auto_retry_dec = auto_retry_dec + manualtab.ManualGatherTab.auto_retry_inc = auto_retry_inc + publictab.UIRow.update = update + publictab.UIRow._clear = _clear + publictab.UIRow.on_stats_click = on_stats_click + publictab.UIRow.popup_menu_closing = popup_menu_closing + publictab.UIRow.popup_menu_selected_choice = popup_menu_selected_choice + # publictab.PublicGatherTab._update_party_lists = _update_party_lists + + +class PartyQuickConnect(bui.Window): + def __init__(self, address: str, port: int): + self._width = 800 + self._height = 400 + self._white_tex = bui.gettexture('white') + self.lineup_tex = bui.gettexture('playerLineup') + self.lineup_1_transparent_mesh = bui.getmesh( + 'playerLineup1Transparent') + self.eyes_mesh = bui.getmesh('plasticEyesTransparent') + uiscale = bui.app.ui_v1.uiscale + super().__init__(root_widget=bui.containerwidget( + size=(self._width, self._height), + color=(0.45, 0.63, 0.15), + transition='in_scale', + scale=(1.4 if uiscale is babase.UIScale.SMALL else + 1.2 if uiscale is babase.UIScale.MEDIUM else 1.0))) + self._cancel_button = bui.buttonwidget(parent=self._root_widget, + scale=1.0, + position=(60, self._height - 80), + size=(50, 50), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.45, 0.63, 0.15), + icon=bui.gettexture('crossOut'), + iconscale=1.2) + bui.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + + self.IP = bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.55 + 60), + size=(0, 0), + color=(1.0, 3.0, 1.0), + scale=1.3, + h_align='center', + v_align='center', + text="IP: "+address + " PORT: "+str(port), + maxwidth=self._width * 0.65) + self._title_text = bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.55), + size=(0, 0), + color=(1.0, 3.0, 1.0), + scale=1.3, + h_align='center', + v_align='center', + text="Retrying....", + maxwidth=self._width * 0.65) + self._line_image = bui.imagewidget( + parent=self._root_widget, + color=(0.0, 0.0, 0.0), + opacity=0.2, + position=(40.0, 120), + size=(800-190+80, 4.0), + texture=self._white_tex) + self.dude_x = 60 + self._body_image_target = bui.buttonwidget( + parent=self._root_widget, + size=(1 * 60, 1 * 80), + color=(random.random(), random.random(), random.random()), + label='', + texture=self.lineup_tex, + position=(40, 110), + mesh_transparent=self.lineup_1_transparent_mesh) + self._eyes_image = bui.imagewidget( + parent=self._root_widget, + size=(1 * 36, 1 * 18), + texture=self.lineup_tex, + color=(1, 1, 1), + position=(40, 165), + mesh_transparent=self.eyes_mesh) + # self._body_image_target2 = bui.imagewidget( + # parent=self._root_widget, + # size=(1* 60, 1 * 80), + # color=(1,0.3,0.4), + # texture=self.lineup_tex, + # position=(700,130), + # mesh_transparent=self.lineup_1_transparent_mesh) + self.closed = False + self.retry_count = 1 + self.direction = "right" + self.connect(address, port) + self.move_R = bs.AppTimer(0.01, babase.Call(self.move_right), repeat=True) + + def move_right(self): + if self._body_image_target and self._eyes_image: + bui.buttonwidget(edit=self._body_image_target, position=(self.dude_x, 110)) + bui.imagewidget(edit=self._eyes_image, position=(self.dude_x+10, 165)) + else: + self.move_R = None + if self.direction == "right": + self.dude_x += 2 + if self.dude_x >= 650: + self.direction = "left" + else: + self.dude_x -= 2 + if self.dude_x <= 50: + self.direction = "right" + + def connect(self, address, port): + if not self.closed and (bs.get_connection_to_host_info_2() == {} or bs.get_connection_to_host_info_2() == None or bs.get_connection_to_host_info_2()['build_number'] == 0): + bui.textwidget(edit=self._title_text, text="Retrying....("+str(self.retry_count)+")") + self.retry_count += 1 + bs.connect_to_party(address, port=port) + self._retry_timer = bs.AppTimer(1.5, babase.Call( + self.connect, address, port)) + + def close(self) -> None: + """Close the ui.""" + self.closed = True + bui.containerwidget(edit=self._root_widget, transition='out_scale') + + +# ba_meta export babase.Plugin +class InitalRun(babase.Plugin): + def __init__(self): + replace() + config = babase.app.config + if config["launchCount"] % 5 == 0: + updateBannedServersCache() diff --git a/plugins/utilities/fast_epic_toggle.py b/plugins/utilities/fast_epic_toggle.py new file mode 100644 index 000000000..6c6733846 --- /dev/null +++ b/plugins/utilities/fast_epic_toggle.py @@ -0,0 +1,50 @@ +# ba_meta require api 9 + +import babase +import bauiv1 as bui +import bauiv1lib.party +import bascenev1 as bs + + +class FastEpicSwitcher(bauiv1lib.party.PartyWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Fast Mode Button + self._fast_btn = bui.buttonwidget( + parent=self._root_widget, + size=(150, 60), + scale=0.7, + label='Fast Mode', + button_type='square', + position=(self._width - 62, self._height - 80), + on_activate_call=self._set_fast_mode + ) + + # Epic Mode Button + self._epic_btn = bui.buttonwidget( + parent=self._root_widget, + size=(150, 60), + scale=0.7, + label='Epic Mode', + button_type='square', + position=(self._width - 62, self._height - 150), + on_activate_call=self._set_epic_mode + ) + + def _set_fast_mode(self): + """Set the game to Fast Mode.""" + bs.get_foreground_host_activity().globalsnode.slow_motion = 0.5 # Fast Mode + bui.screenmessage("Switched to Fast Mode", color=(0, 1, 0)) + + def _set_epic_mode(self): + """Set the game to Epic Mode.""" + bs.get_foreground_host_activity().globalsnode.slow_motion = 1.0 # Epic Mode (Slow) + bui.screenmessage("Switched to Epic Mode!", color=(0, 1, 0)) + +# ba_meta export babase.Plugin + + +class ByANES(babase.Plugin): + def on_app_running(self): + bauiv1lib.party.PartyWindow = FastEpicSwitcher diff --git a/plugins/utilities/file_share.py b/plugins/utilities/file_share.py new file mode 100644 index 000000000..7b4d9be45 --- /dev/null +++ b/plugins/utilities/file_share.py @@ -0,0 +1,376 @@ +# ba_meta require api 9 +''' +File Share Mod for BombSquad 1.7.30 and above. +https://youtu.be/qtGsFU4cgic +https://discord.gg/ucyaesh +by : Mr.Smoothy + +Thanks Dliwk for contributing MultiPartForm.class +''' +from __future__ import annotations +import mimetypes +import json +import re +import io +import uuid + +import bascenev1 as bs +import _baplus +import _babase +import babase +from bauiv1lib.fileselector import FileSelectorWindow +from bauiv1lib.sendinfo import SendInfoWindow +from bauiv1lib.confirm import ConfirmWindow +import bauiv1 as bui +import os +import urllib.request +from threading import Thread +import logging +from babase._general import Call +from typing import TYPE_CHECKING +from baenv import TARGET_BALLISTICA_BUILD +if TYPE_CHECKING: + from typing import Any, Callable, Sequence + +app = _babase.app + + +MODS_DIR = app.python_directory_user if TARGET_BALLISTICA_BUILD < 21282 else app.env.python_directory_user +REPLAYS_DIR = bui.get_replays_dir() +HEADERS = { + 'accept': 'application/json', + 'Content-Type': 'application/octet-stream', + 'User-Agent': 'BombSquad Client' +} + + +class UploadConfirmation(ConfirmWindow): + def __init__( + self, + file_path="", + status="init", + text: str | bui.Lstr = 'Are you sure?', + ok_text="", + action: Callable[[], Any] | None = None, + origin_widget: bui.Widget | None = None, + + ): + super().__init__(text=text, action=action, + origin_widget=origin_widget, ok_text=ok_text) + self.status = status + self.file_path = file_path + + def _ok(self) -> None: + if self.status == "init": + self._cancel() + UploadConfirmation( + "", "uploading", text="Uploading file wait !", ok_text="Wait") + self._upload_file() + + elif self.status == "uploading": + bui.screenmessage("uploading in progress") + elif self.status == "uploaded": + pass + + def _upload_file(self): + self.status = "uploading" + # print(self.root_widget) + thread = Thread(target=handle_upload, args=( + self.file_path, self.uploaded, self.root_widget,)) + thread.start() + + def uploaded(self, url, root_widget): + self.status = "uploaded" + from bauiv1lib.url import ShowURLWindow + ShowURLWindow(url) + + +class InputWindow(SendInfoWindow): + def __init__( + self, modal: bool = True, origin_widget: bui.Widget | None = None, path=None): + super().__init__(modal=modal, legacy_code_mode=True, origin_widget=origin_widget) + bui.textwidget(edit=self._text_field, max_chars=300) + self._path = path + self.message_widget = bui.textwidget( + parent=self._root_widget, + text="put only trusted link", + position=(170, 230 - 200 - 30), + color=(0.8, 0.8, 0.8, 1.0), + size=(90, 30), + h_align='center', + ) + + def _do_enter(self): + url = bui.textwidget(query=self._text_field) + if self._path and self._path != "/bombsquad": + bui.textwidget(edit=self.message_widget, + text="downloading.... wait...") + bui.screenmessage("Downloading started") + thread = Thread(target=handle_download, args=( + url, self._path, self.on_download,)) + thread.start() + else: + bui.textwidget(edit=self.message_widget, + text="First select folder were to save file.") + self.close() + + def on_download(self, output_path): + bui.screenmessage("File Downloaded to path") + bui.screenmessage(output_path) + bui.screenmessage("GO back and reopen to refresh") + + def close(self): + bui.containerwidget( + edit=self._root_widget, transition=self._transition_out + ) + + +class FileSelectorExtended(FileSelectorWindow): + + def __init__( + self, + path: str, + callback: Callable[[str | None], Any] | None = None, + show_base_path: bool = True, + valid_file_extensions: Sequence[str] | None = None, + allow_folders: bool = False, + ): + super().__init__(path, callback=callback, show_base_path=show_base_path, + valid_file_extensions=valid_file_extensions, allow_folders=allow_folders) + self._import_button = bui.buttonwidget( + parent=self._root_widget, + button_type='square', + position=(self._folder_center + 200, self._height - 113), + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + enable_sound=False, + size=(55, 35), + label="Import", + on_activate_call=self._open_import_menu, + ) + + def _open_import_menu(self): + InputWindow(origin_widget=self._import_button, path=self._path) + + def _on_entry_activated(self, entry: str) -> None: + # pylint: disable=too-many-branches + new_path = None + try: + assert self._path is not None + if entry == '..': + chunks = self._path.split('/') + if len(chunks) > 1: + new_path = '/'.join(chunks[:-1]) + if new_path == '': + new_path = '/' + else: + bui.getsound('error').play() + else: + if self._path == '/': + test_path = self._path + entry + else: + test_path = self._path + '/' + entry + if test_path == "/bombsquad/mods": + test_path = MODS_DIR + if test_path == "/bombsquad/replays": + test_path = REPLAYS_DIR + if os.path.isdir(test_path): + bui.getsound('swish').play() + new_path = test_path + elif os.path.isfile(test_path): + if self._is_valid_file_path(test_path): + bui.getsound('swish').play() + if self._callback is not None: + self._callback(test_path) + else: + bui.getsound('error').play() + else: + print( + ( + 'Error: FileSelectorWindow found non-file/dir:', + test_path, + ) + ) + except Exception: + logging.exception( + 'Error in FileSelectorWindow._on_entry_activated().' + ) + + if new_path is not None: + self._set_path(new_path) + + +org_listdir = os.listdir + + +def custom_listdir(path): + if path == "/bombsquad": + return ["mods", "replays"] + return org_listdir(path) + + +os.listdir = custom_listdir + + +class MultiPartForm: + """Accumulate the data to be used when posting a form.""" + + def __init__(self): + self.form_fields = [] + self.files = [] + # Use a large random byte string to separate + # parts of the MIME data. + self.boundary = uuid.uuid4().hex.encode('utf-8') + return + + def get_content_type(self): + return 'multipart/form-data; boundary={}'.format( + self.boundary.decode('utf-8')) + + def add_field(self, name, value): + """Add a simple field to the form data.""" + self.form_fields.append((name, value)) + + def add_file(self, fieldname, filename, fileHandle, + mimetype=None): + """Add a file to be uploaded.""" + body = fileHandle.read() + if mimetype is None: + mimetype = ( + mimetypes.guess_type(filename)[0] or + 'application/octet-stream' + ) + self.files.append((fieldname, filename, mimetype, body)) + return + + @staticmethod + def _form_data(name): + return ('Content-Disposition: form-data; ' + 'name="{}"\r\n').format(name).encode('utf-8') + + @staticmethod + def _attached_file(name, filename): + return ('Content-Disposition: form-data; ' + 'name="{}"; filename="{}"\r\n').format( + name, filename).encode('utf-8') + + @staticmethod + def _content_type(ct): + return 'Content-Type: {}\r\n'.format(ct).encode('utf-8') + + def __bytes__(self): + """Return a byte-string representing the form data, + including attached files. + """ + buffer = io.BytesIO() + boundary = b'--' + self.boundary + b'\r\n' + + # Add the form fields + for name, value in self.form_fields: + buffer.write(boundary) + buffer.write(self._form_data(name)) + buffer.write(b'\r\n') + buffer.write(value.encode('utf-8')) + buffer.write(b'\r\n') + + # Add the files to upload + for f_name, filename, f_content_type, body in self.files: + buffer.write(boundary) + buffer.write(self._attached_file(f_name, filename)) + buffer.write(self._content_type(f_content_type)) + buffer.write(b'\r\n') + buffer.write(body) + buffer.write(b'\r\n') + + buffer.write(b'--' + self.boundary + b'--\r\n') + return buffer.getvalue() + + +def handle_upload(file, callback, root_widget): + file_name = file.split("/")[-1] + with open(file, "rb") as f: + file_content = f.read() + bui.screenmessage("Uploading file, wait !") + form = MultiPartForm() + form.add_file( + 'file', file_name, + fileHandle=io.BytesIO(file_content)) + + # Build the request, including the byte-string + # for the data to be posted. + data = bytes(form) + file_name = urllib.parse.quote(file_name) + r = urllib.request.Request(f'https://file.io?title={file_name}', data=data) + r.add_header('Content-type', form.get_content_type()) + r.add_header('Content-length', len(data)) + + try: + with urllib.request.urlopen(r) as response: + if response.getcode() == 200: + # callback(json.loads(response.read().decode('utf-8'))["link"]) + _babase.pushcall(Call(callback, json.loads(response.read().decode( + 'utf-8'))["link"], root_widget), from_other_thread=True) + else: + bui.screenmessage( + f"Failed to Upload file. Status code: {response.getcode()}") + except urllib.error.URLError as e: + bui.screenmessage(f"Error occurred: {e}") + + +def handle_download(url, path, callback): + req = urllib.request.Request(url, headers={'accept': '*/*'}, method='GET') + try: + with urllib.request.urlopen(req) as response: + if response.getcode() == 200: + # Read the filename from the Content-Disposition header + filename = None + content_disposition = response.headers.get( + 'Content-Disposition', '') + + match = re.search(r'filename\*?=(.+)', content_disposition) + + if match: + filename = urllib.parse.unquote( + match.group(1), encoding='utf-8') + filename = filename.replace("UTF-8''", '') + + output_path = os.path.join(path, filename) + + with open(output_path, 'wb') as file: + file.write(response.read()) + _babase.pushcall(Call(callback, output_path), + from_other_thread=True) + print(f"File downloaded and saved to: {output_path}") + else: + print( + f"Failed to download file. Status code: {response.getcode()}") + except urllib.error.URLError as e: + # bui.screenmessage(f'Error occured {e}') + print(f"Error occurred: {e}") + +# ba_meta export babase.Plugin + + +class bySmoothy(babase.Plugin): + def on_app_running(self): + pass + + def has_settings_ui(self): + return True + + def show_settings_ui(self, source_widget): + virtual_directory_path = '/bombsquad' + FileSelectorExtended( + virtual_directory_path, + callback=self.fileSelected, + show_base_path=False, + valid_file_extensions=[ + "txt", "py", "json", "brp" + ], + allow_folders=False, + ).get_root_widget() + + def fileSelected(self, path): + if path: + UploadConfirmation(path, "init", text="You want to upload " + + path.split("/")[-1], ok_text="Upload") diff --git a/plugins/utilities/fileman.py b/plugins/utilities/fileman.py new file mode 100644 index 000000000..b2e28e3ca --- /dev/null +++ b/plugins/utilities/fileman.py @@ -0,0 +1,1996 @@ +# Copyright 2025 - Solely by BrotherBoard +# Intended for personal use only +# Bug? Feedback? Telegram >> @BroBordd + +""" +FileMan v1.0 - Advanced file manager + +Adds a button to settings menu. +Experimental. Read code to know more. +""" + +from babase import ( + PluginSubsystem as SUB, + Plugin, + env +) +from bauiv1 import ( + get_virtual_screen_size as res, + clipboard_set_text as COPY, + get_string_height as strh, + get_string_width as strw, + get_special_widget as zw, + get_replays_dir as rdir, + containerwidget as cw, + hscrollwidget as hsw, + screenmessage as SM, + buttonwidget as obw, + scrollwidget as sw, + SpecialChar as sc, + imagewidget as iw, + textwidget as tw, + gettexture as gt, + apptimer as teck, + AppTimer as tuck, + getsound as gs, + charstr as cs, + MainWindow, + open_url, + Call, + app +) +from os.path import ( + basename, + getmtime, + splitext, + dirname, + getsize, + exists, + isfile, + isdir, + join, + sep +) +from os import ( + listdir as ls, + getcwd, + rename, + remove, + access, + mkdir, + X_OK, + R_OK +) +from shutil import ( + copytree, + rmtree, + copy, + move +) +from bascenev1 import new_replay_session as REP +from http.client import HTTPSConnection as GO +from datetime import datetime as DT +from mimetypes import guess_type +from random import uniform as UF +from threading import Thread +from pathlib import Path + + +class FileMan(MainWindow): + VER = '1.0' + INS = [] + + @classmethod + def resize(c): + c.clean() + [_.on_resize() for _ in c.INS] + + @classmethod + def clean(c): + c.INS = [_ for _ in c.INS if not _.gn and _.p.exists()] + + @classmethod + def loadc(c): + [setattr(c, f'COL{i}', _) for i, _ in enumerate(var('col'))] + + def __del__(s): + s.__class__.clean() + + def on_resize(s): + [_.delete() for _ in s.killme] + s.killme.clear() + c = s.uploadc + s.spyt = s.sharel = s.buf = s.uploadc = None + if c: + c.close() + s.fresh() + + def __init__(s, src): + s.__class__.clean() + s.__class__.INS.append(s) + s.wop() + s.url = s.urlo = var('cwd') + s.urlbln = s.dro = s.gn = s.rlyd = s.flon = False + s.amoled = sum(s.COL5) == 0 + s.pusho = s.sorti = 0 + s.pushi = -0.1 + s.sl = (None, None) + [setattr(s, _, None) for _ in ['pushe', 'eno', 'leno', 'clp', 'gab', + 'clpm', 'rlydt', 'buf', 'uploadc', 'cursnd', 'rfl', 'rflo']] + [setattr(s, _, []) for _ in ['trash', 'btns', 'secs', 'docs', 'drkids', 'drol', + 'okes', 'fkids', 'ftrash', 'statkids', 'fcons', 'flkids', 'killme']] + s.pushq = '' + # root + s.p = cw( + background=False, + toolbar_visibility='menu_minimal' + ) + s.rect = iw( + texture=gt('softRect'), + opacity=[0.3, 0.85][s.amoled], + color=s.COL5, + parent=s.p + ) + super().__init__( + root_widget=s.p, + transition='in_scale', + origin_widget=src + ) + s.bg = iw( + texture=gt('white'), + parent=s.p, + position=(-2, 0), + color=s.COL5 + ) + s.bg2 = bw( + bg='empty', + parent=s.p, + oac=s.bga + ) + # dock + s.docs = [iw( + color=s.COL1, + parent=s.p, + texture=gt('white'), + opacity=0.5 + ) for _ in range(3)] + # sections + s.secs = [tw( + parent=s.p, + text=_, + h_align='center', + color=s.COL4, + scale=0.7 + ) for _ in ['Action', 'Extra', 'New']] + # actions + s.gab = bw( + parent=s.p, + oac=s.cancel, + size=(0, 0), + selectable=False + ) + r = [] + for _ in range(6): + l = ['Copy', 'Move', 'Delete', 'Share', 'Rename', 'Open'][_] + r.append(bw( + parent=s.p, + label=l, + oac=Call(s.act, 0, _) + )) + s.btns.append(r) + # extra + r = [] + for _ in range(4): + l = ['Star', 'Sort', 'Filter', 'Theme'][_] + r.append(bw( + parent=s.p, + label=l, + oac=Call(s.act, 1, _) + )) + s.btns.append(r) + s.fltxt = tw( + editable=True, + parent=s.p, + v_align='center', + glow_type='uniform', + allow_clear_button=False + ) + s.flh = tw( + parent=s.p, + v_align='center', + color=s.COL0 + ) + s.gab2 = bw( + parent=s.p, + oac=s.unfl, + size=(0, 0), + selectable=False + ) + # new + r = [] + for _ in range(2): + l = ['File', 'Folder'][_] + r.append(bw( + parent=s.p, + label=l, + oac=Call(s.act, 2, _) + )) + s.btns.append(r) + # back + s.bb = bw( + parent=s.p, + label=' '+cs(sc.BACK), + oac=s.bye + ) + cw(s.p, cancel_button=s.bb) + # up + s.ub = bw( + parent=s.p, + label=cs(sc.SHIFT), + oac=s.up + ) + # url + s.urlbg = bw( + parent=s.p, + oac=s.urled + ) + s.urlt = tw( + color=s.COL2, + text=s.url, + v_align='center', + parent=s.p + ) + s.urla = tw( + editable=True, + size=(0, 0), + parent=s.p, + text=s.url + ) + s.trash.append(tuck(0.01, s.urlspy, repeat=True)) + s.trash.append(tuck(0.1, s.urlbl, repeat=True)) + # rf + s.rfb = bw( + parent=s.p, + label=cs(sc.PLAY_BUTTON), + oac=s.rf + ) + # pre + s.preb = bw( + parent=s.p, + label=cs(sc.LOGO_FLAT), + oac=s.pre + ) + # yes + s.yesbg = iw( + parent=s.p, + texture=gt('white'), + color=s.COL1, + opacity=0.5, + position=(20, 20) + ) + s.yesbg2 = iw( + parent=s.p, + texture=gt('white'), + color=s.COL5, + opacity=0.3, + ) + s.yesp1 = sw( + parent=s.p, + border_opacity=0, + position=(17, 20) + ) + s.yesp2 = cw( + parent=s.yesp1, + background=False + ) + s.lmao = tw( + parent=s.yesp2, + text='' + ) + # oke + s.okes = [tw( + parent=s.p, + text=_, + h_align='left', + color=s.COL4, + scale=0.7 + ) for _ in SRT()] + # drop + s.drbg = iw( + texture=gt('white'), + parent=s.p, + opacity=0.7, + color=s.COL5 + ) + s.drp1 = sw( + border_opacity=0, + parent=s.p + ) + s.drp2 = cw( + background=False, + parent=s.drp1 + ) + # push + s.pushbg2 = iw( + color=s.COL5, + parent=s.p, + opacity=0, + texture=gt('softRect') + ) + s.pushbg = iw( + color=s.COL1, + parent=s.p, + opacity=0, + texture=gt('white') + ) + s.pusht = tw( + color=s.COL2, + parent=s.p, + h_align='center', + v_align='center' + ) + s.trash.append(tuck(0.01, s.fpush, repeat=True)) + # finally + s.fresh() + teck(0.5, lambda: s.push(f'FileMan v{s.VER} Ready!', du=1.5) if s.eno is None else 0) + + def meh(s): + if s.sl[0] is None: + s.btw('Select something!') + return 1 + if s.sl[1] == '..': + s.btw('What are you doing blud') + return 1 + + def btw(s, t, du=3): + s.snd('block') + s.push(t, color=s.COL3, du=du) + + def act(s, i, j, gay=False): + if s.gn: + return + w = s.btns[i][j] + match i: + case 0: + match j: + case 0: + if s.clp: + if s.clpm != j: + s.btw("You're already doing something else!") + return + c = var('cwd') + chk = join(c, basename(s.clp)) + st1, st2 = splitext(chk) + nn = st1+'_copy'+st2 if exists(chk) else chk + if exists(nn): + s.btw('A copy of this '+['file', 'folder'] + [isdir(chk)]+' already exists!') + return + try: + [copy, copytree][isdir(s.clp)](s.clp, nn) + except Exception as e: + s.btw(str(e)) + return + else: + GUN() + s.push('Pasted!') + s.clp = None + s.fresh() + else: + if s.meh(): + return + s.clp = s.sl[1] + s.clpm = j + s.push(f'Copied! Now go to destination.') + GUN() + s.fresh(skip=True) + case 1: + if s.clp: + if s.clpm != j: + s.btw("You are already doing something else!") + return + c = var('cwd') + chk = join(c, basename(s.clp)) + if exists(chk): + s.btw('There is a '+['file', 'folder'] + [isdir(chk)]+' with the same name here.') + return + try: + move(s.clp, c) + except Exception as e: + s.btw(str(e)) + return + else: + GUN() + s.push('Pasted!') + s.clp = None + s.fresh() + else: + if s.meh(): + return + s.clp = s.sl[1] + s.clpm = j + s.push(f'Now go to destination and paste.') + GUN() + s.fresh(skip=True) + case 2: + if s.clpm: + s.btw("Finish what you're doing first!") + return + if s.meh(): + return + h = s.sl[1] + bn = basename(h) + if not s.rlyd: + s.beep(1, 0) + s.push(f"Really delete "+["the file '"+bn+"'", "the whole '"+bn+"' folder"] + [isdir(h)]+" forever? Press again to confirm.", du=3, color=s.COL3) + s.rlydt = tuck(2.9, Call(setattr, s, 'rlyd', False)) + s.rlyd = True + return + s.rlyd = False + s.rlydt = None + f = [remove, rmtree][isdir(h)] + try: + f(h) + except Exception as e: + s.btw(str(e)) + return + else: + GUN() + s.push('Deleted!') + s.sl = (None, None) + s.fresh() + case 3: + if s.meh(): + return + f = s.sl[1] + if isdir(f): + s.btw("You can't share a folder!") + return + s.wop() + o = w.get_screen_space_center() + xs, ys = 400, 170 + p = s.uploadp = cw( + parent=zw('overlay_stack'), + scale_origin_stack_offset=o, + stack_offset=o, + size=(xs, ys), + background=False, + transition='in_scale' + ) + s.killme.append(p) + iw( + parent=p, + size=(xs*1.2, ys*1.2), + texture=gt('softRect'), + opacity=[0.2, 0.55][s.amoled], + position=(-xs*0.1, -ys*0.1), + color=s.COL5 + ) + iw( + parent=p, + texture=gt('white'), + color=s.COL1, + opacity=0.7, + size=(xs, ys) + ) + bw( + parent=p, + label='Back', + oac=s.cupload, + position=(30, 15), + size=(xs-60, 30) + ) + s.cpsharelb = bw( + parent=p, + label='...', + oac=s.cpsharel, + position=(30, 50), + size=(xs-60, 30) + ) + s.opsharelb = bw( + parent=p, + label='...', + oac=s.opsharel, + position=(30, 85), + size=(xs-60, 30) + ) + bw( + parent=p, + label='Upload to bashupload.com', + oac=s.upload, + position=(30, 120), + size=(xs-60, 30) + ) + case 4: + if s.meh(): + return + t = s.fkids[s.sl[0]] + fp = s.sl[1] + if s.clp == j: + q = tw(query=t) + try: + if sep in q: + raise ValueError( + "You can't use directory separator in filename!") + Path(q) + except Exception as e: + s.btw(str(e) or 'Invalid filename!') + return + else: + if (basename(fp) == q) and not gay: + s.btw("Write a new name blud") + return + chk = join(var('cwd'), q) + if exists(chk): + s.btw( + f"There is a {['file', 'folder'][isdir(chk)]} with this name already!") + return + else: + nfp = join(dirname(fp), q) + try: + rename(fp, nfp) + except PermissionError: + if exists(nfp): + pass + else: + s.push('Permission denied!') + return + except Exception as e: + s.btw(str(e)) + return + else: + s.push('Renamed!') + s.clp = None + GUN() + s.fresh(sl=nfp) + else: + if s.clpm: + s.btw("You didn't paste yet blud") + return + tw(t, editable=True, color=s.COL7) + cw(s.yesp2, visible_child=t) + s.clpm = s.clp = j + s.push('Now edit the filename, then press Done.') + if s.flon and s.rfl: + [_.delete() for _ in s.flkids[s.sl[0]]] + GUN() + s.fresh(skip=True) + case 5: + if s.meh(): + return + if s.clpm: + s.btw("Press again when you're free!") + return + h = s.sl[1] + bn = basename(h) + if isdir(h): + s.cd(h) + s.snd('deek') + return + s.stat = 1000 + s.wop() + k = s.fkids[s.sl[0]] if gay else w + + def gcen(): return ((o := k.get_screen_space_center()), + (o[0]-s.size[0]/5, o[1]) if gay else o)[1] + o = gcen() + xs, ys = [_*0.6 for _ in s.size] + p = cw( + parent=zw('overlay_stack'), + scale_origin_stack_offset=o, + size=(xs, ys), + background=False, + transition='in_scale' + ) + s.killme.append(p) + iw( + parent=p, + size=(xs*1.2, ys*1.2), + texture=gt('softRect'), + opacity=[0.3, 0.7][s.amoled], + position=(-xs*0.1, -ys*0.1), + color=s.COL5 + ) + iw( + parent=p, + texture=gt('white'), + color=s.COL5, + opacity=0.7, + size=(xs, ys) + ) + b = bw( + parent=p, + position=(20, ys-70), + label=' '+cs(sc.BACK), + size=(50, 50), + oac=Call(s.statbye, p, gcen) + ) + cw(p, cancel_button=b) + ix = xs-250 + iw( + parent=p, + texture=gt('white'), + color=s.COL1, + position=(90, ys-72), + size=(ix, 54), + opacity=0.5 + ) + tw( + parent=p, + h_align='center', + v_align='center', + position=(xs/2-60, ys-60), + text=basename(h), + maxwidth=ix-100 + ) + iw( + parent=p, + texture=gt('white'), + color=s.COL1, + opacity=0.5, + position=(20, 20), + size=(xs-40, ys-110) + ) + bw( + parent=p, + label=cs(sc.REWIND_BUTTON), + position=(xs-141, ys-70), + size=(50, 50), + oac=Call(s.stata, -1), + repeat=True + ) + bw( + parent=p, + label=cs(sc.FAST_FORWARD_BUTTON), + position=(xs-71, ys-70), + size=(50, 50), + oac=Call(s.stata, 1), + repeat=True + ) + s.oops = 0 + try: + with open(h, 'r') as f: + da = f.read() + except Exception as ex: + da = '' + s.oops = 1 + if isinstance(ex, PermissionError): + kek = 'Permission denied!' + elif isinstance(ex, UnicodeDecodeError): + kek = 'No preview avaiable' + else: + kek = str(ex) + else: + if not da: + s.oops = 1 + kek = 'No data' + if not s.oops: + fxs = xs-40 + fys = ys-110 + s.statsz = (fxs, fys) + p0 = s.statp0 = sw( + parent=p, + position=(20, 20), + size=(fxs, fys), + border_opacity=0, + capture_arrows=True + ) + s.statda = da + s.statp = 0 + s.statl = [] + s.itw() + else: + ty = s.gtype(h) + if ty == 'Replay': + tw( + parent=p, + position=(xs/2-20, ys-150), + text='Press start to preview replay.\nKeep in mind that this will destroy the current FileMan session.', + h_align='center', + color=s.COL4, + maxwidth=xs-60 + ) + bw( + parent=p, + label='Start', + oac=lambda: (b.activate(), teck(0.1, s.bye), + teck(0.3, Call(REP, h))), + position=(xs/2-75, ys/2-135), + size=(150, 40) + ) + elif ty == 'Texture' and bn in TEX(): + wd = min(xs-80, ys-150) + tex = gt(splitext(bn)[0]) + iw( + parent=p, + texture=tex, + size=(wd, wd), + position=(xs/2-wd/2, 40) + ) + elif ty == 'Audio' and bn in AUDIO(): + tw( + parent=p, + position=(xs/2-20, ys-150), + text=f'Sound is recognized by filename, not data.\nPress the buttons below to play/pause', + h_align='center', + color=s.COL4, + maxwidth=xs-60 + ) + bw( + parent=p, + label=cs(sc.PLAY_BUTTON), + oac=lambda: (getattr(s.cursnd, 'stop', lambda: 0)(), setattr( + s, 'cursnd', gs(splitext(bn)[0])), s.cursnd.play()), + position=(xs/2-30, ys/2-135), + size=(40, 40) + ) + bw( + parent=p, + label=cs(sc.PAUSE_BUTTON), + oac=lambda: getattr(s.cursnd, 'stop', lambda: 0)(), + position=(xs/2+30, ys/2-135), + size=(40, 40) + ) + else: + tw( + parent=p, + text=kek, + position=(xs/2-25, ys/2-35), + h_align='center', + v_align='center', + maxwidth=xs-100 + ) + case 1: + match j: + case 0: + star = var('star') + c = var('cwd') + if c in star: + star.remove(c) + s.push('Unstarred!') + else: + star.append(c) + s.push('Starred! (bomb top right)') + var('star', star) + GUN() + s.fresh(skip=True) + case 1: + xs, ys = 200, 230 + s.wop() + + def gcen(): return ((o := w.get_screen_space_center()), + (o[0]-s.size[0]/5, o[1]) if gay else o)[1] + o = gcen() + p = cw( + parent=zw('overlay_stack'), + scale_origin_stack_offset=o, + size=(xs, ys), + background=False, + transition='in_scale', + stack_offset=(o[0], o[1]), + on_outside_click_call=lambda: (cw(p, transition='out_scale'), s.laz()) + ) + s.killme.append(p) + iw( + parent=p, + size=(xs*1.2, ys*1.2), + texture=gt('softRect'), + opacity=[0.3, 0.7][s.amoled], + position=(-xs*0.1, -ys*0.1), + color=s.COL5 + ) + iw( + parent=p, + texture=gt('white'), + color=s.COL5, + opacity=0.7, + size=(xs, ys) + ) + by = 40 + srt = SRT() + for _ in range(4): + bw( + position=(20, ys-20-by-(by+10)*_), + size=(xs-40, by), + label=srt[_], + oac=Call(s.surt, _, p), + parent=p + ) + case 2: + s.flon = True + s.snd('deek') + s.fresh(skip=True) + case 3: + ox, oy = s.size + xs = ys = min(ox / 2, oy / 2) + xs *= 1.3 + s.wop() + s.push( + 'FileMan uses 12 main colors. Tap on a color to edit it. Press outside to cancel.', du=6) + o = w.get_screen_space_center() + + def nuke(): + cw(p, transition='out_scale') + s.laz() + s.push('Cancelled! Nothing was saved') + p = cw(parent=zw('overlay_stack'), scale_origin_stack_offset=o, size=( + xs, ys), stack_offset=(-100, 0), background=False, transition='in_scale', on_outside_click_call=nuke) + bw(parent=p, size=(xs+200, ys), bg='empty') + s.killme.append(p) + iw(parent=p, size=(xs * 1.2, ys * 1.2), texture=gt('softRect'), + opacity=[0.3, 0.7][s.amoled], position=(-xs * 0.1, -ys * 0.1), color=s.COL5) + iw(parent=p, texture=gt('white'), color=s.COL5, + opacity=0.7, size=(xs + 200, ys)) + temp_colors, scl, sl = [getattr(s, f'COL{i}') for i in range(12)], [ + 0, 0, 0, 0], 0 + kids, nubs, grad = [], [], [] + + def save(): + if var('col') == temp_colors: + s.btw('At least change a color blud') + return + var('col', temp_colors) + GUN() + cw(p, transition='out_scale') + s.__class__.loadc() + s.bye() + SM('Reopen FileMan to see changes!') + + def update_previews(): + f3() + f4() + c = temp_colors[sl] + obw(kids[sl], color=c, textcolor=INV(c)) + + def f3(): + [iw(l, position=(xs + scl[k] * ps - 16, 39 + qs * 5 - (qs) * k)) + for k, l in enumerate(nubs)] + + def f4(): + c = temp_colors[sl] + [obw(l, color=(c[0] * (k / 19), c[1] * (k / 19), c[2] * (k / 19))) + for k, l in enumerate(grad)] + + def f2(k, l): + nonlocal scl + scl[k] = l + val = l / 19.0 + if k < 3: + c_list = list(temp_colors[sl]) + c_list[k] = val + temp_colors[sl] = new_color = tuple(c_list) + scl[3] = int(max(new_color) * 19) + elif k == 3: + c = temp_colors[sl] + current_max = max(c) + if current_max > 0: + scale = val / current_max + temp_colors[sl] = new_color = ( + c[0] * scale, c[1] * scale, c[2] * scale) + scl[:3] = [int(x * 19) for x in new_color] + update_previews() + + def f(z, sh=0): + nonlocal sl, scl + [obw(_, label='') for _ in kids] + obw(kids[z], label=cs(sc.DPAD_CENTER_BUTTON)) + sl = z + if not sh: + s.snd('deek') + c = temp_colors[sl] + scl[:3] = [int(x * 19) for x in c] + scl[3] = int(max(c) * 19) if any(c) else 0 + update_previews() + bs, qs, ps = (ys - 60) / 3, (ys - 60) / 6, 9 + for k in range(4): + for l in range(20): + ah = l / 19.0 + b = obw( + parent=p, position=(xs + l * ps, 47 + qs * 5 - qs * k), size=(ps + 2, qs / 2), label='', texture=gt('white'), enable_sound=False, on_activate_call=Call(f2, k, l), + color=((ah, 0, 0) if k < 1 else (0, ah, 0) if k < + 2 else (0, 0, ah) if k < 3 else (ah, ah, ah)) + ) + if k == 3: + grad.append(b) + nubs = [iw(parent=p, size=(35, 35), texture=gt('nub'), + color=(10, 10, 10), opacity=0.4) for _ in range(4)] + for x in range(4): + for y in range(3): + z = x * 3 + y + c = temp_colors[z] + kids.append(bw(parent=p, position=( + 20 + (bs + 10) * x, 20 + (bs + 10) * y), size=(bs, bs), color=c, textcolor=INV(c), oac=Call(f, z))) + bw(parent=p, position=(xs + 5, 24 + qs), + size=(172, qs - 2), label='Save', oac=save) + + def reset(): + mem = COL() + if mem == temp_colors: + s.btw("Reset what? It's already at default") + return + for i, m in enumerate(mem): + temp_colors[i] = m + update_previews() + GUN() + s.push('Restored default colors! now press save') + bw(parent=p, position=(xs + 5, 18.5), + size=(172, qs - 3), label='Reset', oac=reset) + f(0, sh=1) + case 2: + match j: + case 0: + if s.clpm: + s.btw("You're already in the middle of something") + return + c = var('cwd') + n = join(c, 'new_file') + while exists(n): + n += '_again' + try: + Path(n).touch() + except PermissionError: + s.btw('Permission denied!') + return + except Exception as ex: + s.btw(str(ex)) + return + s.fresh(sl=n) + # rename + s.act(0, 4, gay=True) + case 1: + if s.clpm: + s.btw("You're already in the middle of something") + return + c = var('cwd') + n = join(c, 'new_folder') + while exists(n): + n += '_again' + try: + mkdir(n) + except PermissionError: + s.btw('Permission denied!') + return + except Exception as ex: + s.btw(str(ex)) + return + s.fresh(sl=n) + # rename + s.act(0, 4) + + def surt(s, _, p): + if _ == s.sorti: + s.btw('Already sorted by '+SRT()[_]+'!') + return + s.sorti = _ + GUN() + cw(p, transition='out_scale') + s.fresh(sl=s.sl[1]) + + def statbye(s, p, gcen): + try: + cen = gcen() + except: + p.delete() + else: + cw(p, transition='out_scale', scale_origin_stack_offset=cen) + s.laz() + s.statda = None + s.statl = [] + s.statp = 0 + + def itw(s): + PYTHON_KEYWORDS = { + 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', + 'del', 'elif', 'else', 'except', 'False', 'finally', 'for', 'from', + 'global', 'if', 'import', 'in', 'is', 'lambda', 'None', 'nonlocal', + 'not', 'or', 'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield' + } + PYTHON_BUILTINS = { + 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', + 'chr', 'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod', + 'enumerate', 'eval', 'exec', 'filter', 'float', 'format', 'frozenset', 'getattr', + 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', + 'issubclass', 'iter', 'len', 'list', 'locals', 'map', 'max', 'memoryview', + 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', + 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', + 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip' + } + OPERATORS = {'+', '-', '*', '/', '%', '=', '!', '<', '>', '&', '|', '^', '~'} + BRACKETS = {'(', ')', '{', '}', '[', ']', ':', ',', '.'} + + s.COL_KEYWORD = getattr(s, 'COL_KEYWORD', (1.5, 0.9, 0.4)) + s.COL_BUILTIN = getattr(s, 'COL_BUILTIN', (0.7, 1.2, 1.8)) + s.COL_STRING = getattr(s, 'COL_STRING', (0.5, 1.5, 0.5)) + s.COL_NUMBER = getattr(s, 'COL_NUMBER', (1.2, 1.2, 0.5)) + s.COL_OPERATOR = getattr(s, 'COL_OPERATOR', (1.5, 1.5, 1.5)) + s.COL_BRACKET = getattr(s, 'COL_BRACKET', (0.9, 0.9, 0.9)) + s.COL_END_MARKER = getattr(s, 'COL_END_MARKER', (1.8, 0.6, 0.6)) # Neon Red for the marker + + da = s.statda + end_marker = f'[End of chunk | Press {cs(sc.FAST_FORWARD_BUTTON)}]' + if len(da) > s.stat: + if len(da) > s.statp + int(s.stat / 2): + da = da[s.statp:s.stat + s.statp] + end_marker + else: + da = da[s.statp:s.stat + s.statp] + az = sum(s.statl) + lines = [_.replace('\\n', '\\'+"~"+"n") for _ in da.splitlines()] + zc = len(str(az + len(lines))) + da = '\\n'.join([f"{str(i+1+az).zfill(zc)} | {_}" for i, _ in enumerate(lines)]) + z = len(da) + p0 = s.statp0 + fxs, fys = s.statsz + [_.delete() for _ in s.statkids] + s.statkids.clear() + hh = 35 + m = max(da.replace('\\n', '') or ' ', key=GSW) + l = GSW(str(m)) / 1.5 + l = max(15, l) + l = min(l, 20) + das = da.split('\\n') + mm = len(max(das, key=len) or '') + ldas = len(das) + s.statl.append(ldas) + rxs = max(l * mm + 30, fxs) + rys = max(hh * ldas, fys - 15) + pos = (0, rys - 40) + po = list(pos) + q0 = cw(parent=p0, size=(fxs, rys), background=False) + p1 = hsw(parent=q0, size=(fxs, rys), border_opacity=0) + q1 = cw(parent=p1, background=False, size=(rxs, fys)) + s.statkids += [q0, p1, q1] + + # --- Main Rendering Loop --- + i = 0 + nc = 0 + in_triple_comment = False + triple_quote_char = None + is_first_char_offset_applied = False # Flag for the critical offset + off = zc + 3 + try: + mud = int(da[off] == '#') + except IndexError: + mud = 0 + + while i < z: + # -- Priority 1: The End Marker -- + if not in_triple_comment and not mud and da.startswith(end_marker, i): + if not is_first_char_offset_applied: + po[0] -= l * 1.5 + is_first_char_offset_applied = True + for c in end_marker: + big = nc < zc + po[0] += l + s.statkids.append(tw(text=c, position=( + po[0], po[1] - (3 if big else 0)), h_align='center', v_align='top', parent=q1, big=big, color=s.COL_END_MARKER)) + nc += 1 + i += len(end_marker) + continue + + # -- Priority 2: Multi-line comment delimiters -- + if i + 2 < z and da[i:i+3] in ('"'*3, "'"*3): + chunk = da[i:i+3] + if not in_triple_comment or chunk == triple_quote_char * 3: + if not is_first_char_offset_applied: + po[0] -= l * 1.5 + is_first_char_offset_applied = True + if not in_triple_comment: + in_triple_comment = True + triple_quote_char = chunk[0] + else: + in_triple_comment = False + triple_quote_char = None + for _ in range(3): + big = nc < zc + po[0] += l + s.statkids.append(tw(text=da[i], position=( + po[0], po[1] - (3 if big else 0)), h_align='center', v_align='top', parent=q1, big=big, color=s.COL3 if big else s.COL0)) + nc += 1 + i += 1 + continue + + # -- Priority 3: Newlines -- + if i + 1 < z and da[i:i+2] == '\\n': + po[0] = pos[0]-l*1.5 + po[1] -= hh + nc = 0 + try: + mud = int(da[i + 2 + off] == '#' and not in_triple_comment) + except IndexError: + mud = 0 + i += 2 + continue + + # -- Priority 4: Render based on state (comment or code) -- + # Apply the critical offset before rendering the first character + if not is_first_char_offset_applied: + po[0] -= l * 1.5 + is_first_char_offset_applied = True + + if in_triple_comment or mud: + big = nc < zc + color = (s.COL0 if big else s.COL3) if in_triple_comment else ( + s.COL10 if big else s.COL11) + po[0] += l + s.statkids.append(tw(text=da[i], position=( + po[0], po[1] - (3 if big else 0)), h_align='center', v_align='top', parent=q1, big=big, color=color)) + nc += 1 + i += 1 + continue + + # -- Priority 5: Python Syntax Highlighting -- + char = da[i] + token = char + token_color = s.COL2 + if char in ("'", '"'): + k = i + 1 + while k < z and (da[k] != char or da[k-1] == '\\'): + k += 1 + token = da[i:k+1] + token_color = s.COL_STRING + elif char.isdigit() or (char == '.' and i + 1 < z and da[i+1].isdigit()): + k = i + while k < z and (da[k].isdigit() or da[k] == '.'): + k += 1 + token = da[i:k] + token_color = s.COL_NUMBER + elif char.isalpha() or char == '_': + k = i + while k < z and (da[k].isalnum() or da[k] == '_'): + k += 1 + token = da[i:k] + if token in PYTHON_KEYWORDS: + token_color = s.COL_KEYWORD + elif token in PYTHON_BUILTINS: + token_color = s.COL_BUILTIN + elif char in OPERATORS: + token_color = s.COL_OPERATOR + elif char in BRACKETS: + token_color = s.COL_BRACKET + + for c in token: + big = nc < zc + po[0] += l + s.statkids.append(tw(text=c, position=( + po[0], po[1] - (3 if big else 0)), h_align='center', v_align='top', parent=q1, big=big, color=token_color)) + nc += 1 + i += len(token) + + cw(q0, visible_child=tw(parent=q0, text='', position=(0, rys))) + cw(q1, visible_child=tw(parent=q1, text='', position=(0, rys))) + + def stata(s, i): + if not s.oops: + n = s.statp + s.stat*i + ok = len(s.statda) + if ok <= n: + s.btw('Reached EOF!', du=1) + return + if n < 0: + s.btw('Already at first chunk!', du=1) + return + s.snd('deek') + s.statp = n + if i < 0: + [s.statl.pop(-1) for _ in [0, 0]] + s.itw() + else: + s.btw('No more open file buffers!') + + def cpsharel(s): + l = s.vsharel() + if not l: + return + COPY(str(l)) + s.ding(1, 1) + s.push(f"Copied '{l}' to clipboard!") + + def opsharel(s): + l = s.vsharel() + if not l: + return + s.snd('deek') + open_url(l) + + def vsharel(s): + l = getattr(s, 'sharel', 0) + if not l: + s.btw("Upload first!") + return + return l + + def cupload(s): + c = s.uploadc + s.spyt = s.sharel = s.buf = s.uploadc = None + if c: + c.close() + s.ding(0, 0) + s.push('Cancelled!') + else: + s.laz() + cw(s.uploadp, transition='out_scale') + + def upload(s): + f = s.sl[1] + s.ding(1, 0) + s.push('Uploading...') + Thread(target=Call(s._upload, f)).start() + s.spyt = tuck(0.2, Call(s.spy, s.on_upload), repeat=True) + + def _upload(s, l): + try: + c = s.uploadc = GO('bashupload.com') + filename = basename(l) + url_path = '/' + filename + with open(l, 'rb') as f: + body = f.read() + headers = {'Content-Type': 'application/octet-stream'} + c.request('POST', url_path, body=body, headers=headers) + s.buf = c.getresponse().read().decode() + except Exception: + if s.uploadc: + s.buf = '' + finally: + if s.uploadc: + s.uploadc.close() + s.uploadc = s.sharel = None + + def on_upload(s, t): + if not t: + s.btw("Couldn't upload") + return + s.sharel = t.splitlines()[5][5:]+'?download=1' + s.ding(0, 1) + s.push('Success!') + obw(s.cpsharelb, label='Copy Direct URL') + obw(s.opsharelb, label=s.sharel) + + def ding(s, i, j): + a = ['Small', ''] + x, y = a[i], a[j] + s.snd('ding'+x) + teck(0.1, gs('ding'+y).play) + + def beep(s, i, j): + s.snd(f'raceBeep{str(i+1)}') + teck(0.1, gs(f'raceBeep{str(j+1)}').play) + + def spy(s, f): + if s.buf is None: + return + b = s.buf + s.buf = None + f(b) + s.spyt = None + + def cancel(s): + c = s.clp + s.clp = None + s.push('Cancelled!') + s.snd('deek') + s.fresh(skip=c != 4) + + def fresh(s, skip=False, sl=None): + if s.gn: + return + rx, ry = res() + z = s.size = (rx*0.8, ry*0.8) + x, y = z + # root + cw(s.p, size=z) + iw(s.bg, size=z) + obw(s.bg2, size=z) + iw(s.rect, size=(x*1.2, y*1.2), position=(-x*0.1, -y*0.1)) + # docks, secs, btns + h = (x-80) + f = y-191 + v = 18 + for i in range(3): + e = h/[2, 3, 6][i] + iw(s.docs[i], size=(e, 100), position=(v, f)) + tw(s.secs[i], position=(v+e/2-23, f+1)) + a = s.btns[i] + l = int(len(a)/2) + bh = (e-[6, 5, 4][i]*10-(l-1)*10)/l + for j in range(l): + for k in [0, l]: + zz = bh + of = 20 + ga = ( + (i == k == 0 and s.clpm == j) + or + (i == 0 and j == 1 and k == 3 and s.clpm == 4) + ) + if ga and s.clp: + zz -= 40 + of -= 2 + po = v+of+(bh+20)*j, f+60-30*bool(k) + ww = a[j+k] + obw(ww, position=po, size=(zz, 25)) + if not ga: + if i == 1 and j == k == 0: + obw(ww, label=['Star', 'Unstar'][var('cwd') in var('star')]) + elif i == 1 and j == 0 and k: + tw(s.fltxt, position=(po[0]-2, po[1]-2)) + tw(s.flh, position=(po[0], po[1]-2), max_height=37, maxwidth=zz-42) + obw(s.gab2, position=(po[0]+zz-32, po[1])) + if s.flon: + obw(ww, size=(0, 0), label='') + tw(s.fltxt, size=(zz-38, 27)) + tw(s.flh, text='' if s.rfl else 'Write something...') + obw(s.gab2, size=(bh-(zz-38), 25), label='X') + if not s.fltk: + s.rflo = s.fltk = '' + s.fltk = tuck(0.1, s.onfl, repeat=True) + else: + obw(ww, size=(zz, 25), label='Filter') + tw(s.fltxt, size=(0, 0), text='') + tw(s.flh, text='') + obw(s.gab2, size=(0, 0), label='') + s.rfl = s.rflo = s.fltk = None + continue + he = bool(s.clp) + obw(s.gab, position=(po[0]+zz+11, po[1]), + size=(bh-3-zz, 25), label=['', 'X'][he], selectable=he) + obw(ww, label=[['Copy', 'Move', 0, 0, 'Rename'] + [s.clpm], ['Paste', 'Done'][s.clpm == 4]][he]) + if not he: + s.clpm = None + v += e+20 + # back + f = y-70 + obw(s.bb, position=(20, f), size=(50, 50)) + # up + obw(s.ub, position=(90, f), size=(50, 50)) + # url + e = x - 398 + obw(s.urlbg, size=(e, 50), position=(195, f)) + tw(s.urlt, position=(180, y-60), maxwidth=x-370) + # rf + obw(s.rfb, position=(251+e, f), size=(50, 50)) + # pre + obw(s.preb, position=(323+e, f), size=(50, 50)) + # skip the rest + if skip: + return + # drop + s.droc() + # oke + fly = 35 + sx, sy = x-37, y-230 + h = sx/6 + v = 30 + rat = [3, 1, 1, 1] + for i, _ in enumerate(s.okes): + j = rat[i] + tw(_, position=(v+[30, 0][i != 0], sy-15)) + v += h*j + # push + s.rpush() + # files + p = s.yesp2 + [_.delete() for _ in s.fkids] + s.fkids.clear() + [_.delete() for _ in s.ftrash] + s.ftrash.clear() + [_.delete() for _ in s.fcons] + s.fcons.clear() + [[i.delete() for i in j] for j in s.flkids] + s.flkids.clear() + fl = s.gfull() + u = s.rfl + if s.flon and s.rfl: + fl = [_ for _ in fl if (_ == '..') or (u in basename(_))] + cur = s.sl[1] + if cur: + if cur in fl: + sl = cur + else: + s.sl = (None, None) + # yes + rsy = len(fl)*fly + sw(s.yesp1, size=(sx, sy-40)) + cw(s.yesp2, size=(sx, rsy)) + tw(s.lmao, position=(0, rsy)) + iw(s.yesbg, size=(sx, sy)) + iw(s.yesbg2, size=(sx, 40), position=(20, sy-20)) + # files + for i, _ in enumerate(fl): + if _ == sl: + s.sl = (i, _) + v = 15 + hm = s.gdata(_) + for k in range(4): + j = rat[k] + e = h*j + ee = [30, 0][k != 0] + po = (v+ee, rsy-fly-fly*i) + t = tw( + parent=p, + size=(e-15-ee, fly), + position=po, + text=hm[k], + maxwidth=e-15-ee, + v_align='center', + selectable=True, + click_activate=True, + on_activate_call=Call(s._sl, i, _, fl), + glow_type='uniform', + allow_clear_button=False + ) + if s.flon and u and not k: + ci = 0 + bn = basename(_) + ret = [] + while True: + nxt = bn.find(u, ci) + if nxt == -1: + break + bf = bn[:nxt] + ret.append(tw( + parent=p, + text=u, + position=(po[0]+GSW(bf), po[1]+3), + v_align='center' + )) + ci = nxt + len(u) + s.flkids.append(ret) + if ee: + s.fcons.append(iw( + position=(po[0]-ee-8, po[1]+1), + texture=s.gtex(_), + size=(ee+1, ee+1), + parent=p + )) + v += e + if k: + s.ftrash.append(t) + else: + s.fkids.append(t) + if _ == sl: + cw(s.yesp2, visible_child=t) + s.slco(fl) + + def onfl(s): + s.rfl = tw(query=s.fltxt) + if s.rfl != s.rflo: + s.rflo = s.rfl + s.fresh(sl=s.sl[1]) + + def unfl(s): + s.flon = False + s.snd('deek') + s.fresh(sl=s.sl[1]) + + def gtex(s, _): + ty = s.gtype(_) + t = ( + 'replayIcon' if _ == '..' else + 'folder' if isdir(_) else + 'tv' if ty == 'Replay' else + 'audioIcon' if ty == 'Audio' else + 'graphicsIcon' if ty == 'Texture' else + 'star' if ty == 'Mesh' else + 'achievementOutline' if ty == 'Font' else + 'file' + ) + return gt(t) + + def slco(s, fl): + # cancel rename + if s.clp == 4: + s.cancel() + sli = s.sl[0] + for i, g in enumerate(zip(fl, s.fkids, s.fcons)): + _, w, r = g + c = [(s.COL10, s.COL11), (s.COL8, s.COL9)][isdir(_)][sli == i] + tw(w, color=c, editable=False) + iw(r, color=c) + for i, z in enumerate(s.flkids): + for j in z: + tw(j, color=[s.COL0, s.COL3][sli == i]) + + def _sl(s, i, _, fl): + if s.sl[0] == i: + if isdir(_): + s.cd(_) + else: + s.act(0, 5, gay=True) + return + s.sl = (i, _) + s.slco(fl) + + def gdata(s, _): + b = isdir(_) + try: + mt = DT.fromtimestamp(getmtime(_)).strftime('%m/%d/%Y %I:%M %p') + except: + mt = '?' + try: + sz = FMT(getsize(_)) + except: + sz = '?' + return ( + basename(_), + s.gtype(_), + '' if b else mt, + '' if b else sz + ) + + def gtype(s, _): + if isdir(_): + return ['Folder', 'Parent'][_ == '..'] + f = 'File' + h = guess_type(_)[0] or f + if not '.' in _: + return h.title() + if h == f: + return { + 'brp': 'Replay', + 'bob': 'Mesh', + 'cob': 'Mesh', + 'ogg': 'Audio', + 'ktx': 'Texture', + 'fdata': 'Font' + }.get(_.split('.')[-1], f) + else: + return h.title() + + def gfull(s): + c = var('cwd') + h = [] + if dirname(c) != c: + h = ['..'] + if not access(c, R_OK): + return h + items = [join(c, _) for _ in ls(c)] + da = {} + for item in items: + name, item_type, date_modified_str, _ = s.gdata(item) + try: + date_sortable = DT.strptime( + date_modified_str, '%m/%d/%Y %I:%M %p') if date_modified_str else DT.min + except: + date_sortable = DT.min + try: + mt = getmtime(item) + except: + mt = 0 + da[item] = (basename(item).lower(), item_type.lower(), date_sortable, mt, isdir(item)) + return h + sorted(items, key=lambda i: ( + not da[i][4], + da[i][0] if s.sorti == 0 else + da[i][1] if s.sorti == 1 else + da[i][2] if s.sorti == 2 else + da[i][3] + )) + + def pre(s): + s.wop() + r = s._pre() + xs = 200 + ys = 160 + pc = s.preb.get_screen_space_center() + p = s.prep = cw( + parent=zw('overlay_stack'), + background=False, + transition='in_scale', + scale_origin_stack_offset=pc, + on_outside_click_call=lambda: (cw(p, transition='out_scale'), s.laz()), + size=(xs, ys), + stack_offset=(pc[0]-xs/2+27, pc[1]-ys/2+27) + ) + s.killme.append(p) + iw( + parent=p, + size=(xs*1.2, ys*1.2), + texture=gt('softRect'), + opacity=[0.2, 0.55][s.amoled], + position=(-xs*0.1, -ys*0.1), + color=s.COL5 + ) + iw( + parent=p, + size=(xs, ys), + texture=gt('white'), + color=s.COL1, + opacity=0.7 + ) + p2 = sw( + parent=p, + size=(xs, ys) + ) + rys = 30*len(r) + p3 = cw( + parent=p2, + size=(xs, max(ys, rys)), + background=False + ) + for i, _ in enumerate(r): + j, k = _ + tw( + parent=p3, + size=(xs, 30), + position=(0, rys-30-30*i), + maxwidth=xs-20, + text=j, + click_activate=True, + selectable=True, + glow_type='uniform', + on_activate_call=Call(s.pres, k) + ) + + def pres(s, k): + GUN() + cw(s.prep, transition='out_scale') + s.cd(k) + + def _pre(s): + e = app.env + c = e.cache_directory + d = dirname + f = join(d(c), 'ballistica_files', 'ba_data') + g = cs(sc.LOGO_FLAT)+' ' + return [ + *[(cs(sc.DPAD_CENTER_BUTTON)+' '+(basename(_) or _), _) for _ in var('star')], + (g+'Mods', e.python_directory_user), + (g+'Replays', rdir()), + (g+'Config', e.config_directory), + (g+'Cache', c), + (g+'Files', f), + (g+'Python', join(f, 'python')), + (g+'Meshes', join(f, 'meshes')), + (g+'Audio', join(f, 'audio')), + (g+'Textures', join(f, 'textures')) + ] + + def rf(s): + s.snd('ding') + s.fresh() + c = var('cwd') + s.push('Refreshed '+(basename(c) or c)) + + def up(s): + o = var('cwd') + n = dirname(o) + if o == n: + s.eno = 2 + s.nah() + s.snd('block') + return + s.cd(n) + s.snd('deek') + + def glike(s): + c = var('cwd') + if not access(c, R_OK): + s.eno = not access(c, X_OK) + s.nah() + return [] + a = ls(c) + f = [] + for _ in a: + j = join(c, _) + if isdir(j): + f.append(j) + r = [_ for _ in f if _.startswith(s.url)] + return r + + def nah(s): + if s.eno == s.leno: + return + s.leno = s.eno + s.push([ + "I can't list files here! Write next folder name manually.", + "I can't even enter there! Select another folder.", + "Already reached root!" + ][s.eno], color=s.enoc(), du=[3, 3, 2][s.eno]) + + def enoc(s): + return [ + s.COL7, + s.COL4, + s.COL2 + ][s.eno] + + def drop(s, i): + if s.gn: + return + s.dro = i > 0 + s.droc() + + def droc(s): + s.drol = s.glike() + s.rdrop() + + def rdrop(s): + if s.gn: + return + [_.delete() for _ in s.drkids] + s.drkids.clear() + l = len(s.drol) + if not s.dro or not s.drol or (l == 1 and s.drol[0] == s.url): + iw(s.drbg, size=(0, 0)) + sw(s.drp1, size=(0, 0)) + return + x, y = s.size + of = 20 + ys = 30*l+of + fys = min(300, ys) + yp = y-71-fys + xs = x-325 + xp = 160 + iw(s.drbg, size=(xs, fys), position=(xp, yp)) + sw(s.drp1, size=(xs, fys), position=(xp, yp)) + cw(s.drp2, size=(xs, ys-of)) + for i, _ in enumerate(s.drol): + p = (0, ys-30-30*i-of) + s.drkids.append(tw( + parent=s.drp2, + position=p, + text=_, + color=s.COL9, + selectable=True, + click_activate=True, + glow_type='uniform', + on_activate_call=Call(s.cd, _), + size=(GSW(_), 30) + )) + s.drkids.append(tw( + parent=s.drp2, + position=p, + text=s.url, + color=s.COL4, + )) + + def push(s, t, color=None, du=2): + if s.gn: + return + s.rly = False + s.rlydt = None + tw(s.pusht, color=color or s.COL2) + s.pushe = tuck(du, s.upush) + s.pushi = 0.05 + s.pushq = t + s.rpush() + + def upush(s): + if s.gn: + return + s.pushi = -abs(s.pushi) + s.pushq = '' + s.rpush(1) + + def rpush(s, mode=0): + if s.gn: + return + if mode: + tw(s.pusht, text=s.pushq, color=s.COL2) + return + x = s.size[0] + t = s.pushq + w = GSW(t+' '*3) + iw(s.pushbg, size=(w, 30), position=(x/2-w/2, 40)) + iw(s.pushbg2, size=(w*1.1, 30*1.2), position=((x/2-w/2)-w*0.05, (40)-30*0.1)) + tw(s.pusht, text=t, maxwidth=w*0.95, position=(x/2-25, 40)) + + def fpush(s): + if s.gn: + return + n = s.pusho + s.pushi + if not (1 >= n >= 0): + return + s.pusho = n + iw(s.pushbg, opacity=n) + iw(s.pushbg2, opacity=[n*0.4, n][s.amoled]) + + def urlbl(s): + if s.gn: + return + if s.p.get_selected_child() not in [s.ub, s.drp2, s.drp1, s.urlbg]+s.drkids: + s.urlbln = False + if s.dro: + s.drop(-1) + return + s.urlbln = not s.urlbln + if not s.dro: + s.drop(1) + + def urlspy(s): + if s.gn: + return + s.url = tw(query=s.urla) + b1 = exists(s.url) + b2 = isdir(s.url) + g1 = access(var('cwd'), R_OK) + g2 = access(s.url, X_OK) + b = b1 and b2 and g1 and g2 + av = not b1 and g1 and not g2 and s.drol + if b or av: + s.eno = None + q = s.url != s.urlo + if q: + s.droc() + lurl = var('cwd') + can = b1 and b2 and s.url != lurl + if can: + s.cd(s.url) + lurl = s.url + co = ( + s.COL2 if b else + s.COL3 if av else + s.COL6 if not b1 else + s.enoc() + ) + tw(s.urlt, text=s.url+[' ', '|'][s.urlbln or q], color=co) + s.urlo = s.url + if can or isdir(s.url): + return + # complete + f = dirname(s.url) + if not exists(f): + return + if f == lurl: + return + s.cd(f, dry=True) + + def cd(s, t, dry=False): + if t == '..': + t = dirname(var('cwd')) + s.sl = (None, None) + if s.flon and s.rfl: + s.push("Filter is active! Press 'X' to cancel.", du=1.2, color=s.COL3) + var('cwd', t) + if s.eno != 1 and not access(t, X_OK): + s.eno = 1 + s.nah() + 0 if dry else tw(s.urla, text=t) + cw(s.yesp2, visible_child=s.lmao) + s.fresh() + + def urled(s): + if s.gn: + return + s.snd('deek') + s.urla.activate() + + def wop(s): + s.snd('powerup01') + + def laz(s): + s.snd('laser') + + def bye(s): + s.gn = True + s.trash.clear() + s.laz() + s.main_window_back() + s.__class__.clean() + del s + + def snd(s, t): + s.sn = gs(t) + s.sn.play() + teck(UF(0.13, 0.15), s.sn.stop) + + def bga(s): + s.urlbln = False + + +# Tools and Resources +# Lambda means it won't be stored in memory unless called +def UI(): return app.ui_v1 + + +def SCL(a, b, c=None): return [a, b, c][UI().uiscale.value] or b +def GSW(t): return strw(t, suppress_warning=True) +def GSH(t): return strh(t, suppress_warning=True) + + +def FMT(size): return ( + f"{size / 1024**3:.1f} GB" if size >= 1024**3 else ( + f"{size / 1024**2:.1f} MB" if size >= 1024**2 else ( + f"{size / 1024:.1f} KB" if size >= 1024 else f"{size} B")) +) +def GUN(): return gs('gunCocking').play() + + +def BASE(): return join(dirname(app.env.cache_directory), 'ballistica_files', 'ba_data') +def AUDIO(): return ls(join(BASE(), 'audio')) +def TEX(): return ls(join(BASE(), 'textures')) + + +def SRT(): return ['Name', 'Type', 'Date Modifed', 'Size'] +def INV(c): return ((1-c[0])*2, (1-c[1])*2, (1-c[2])*2) + + +def COL(): return [ + (0.5, 0.5, 0), + (0.17, 0.17, 0.17), + (1, 1, 1), + (1, 1, 0), + (0.6, 0.6, 0.6), + (0, 0, 0), + (1, 0, 0), + (1, 0, 1), + (0.5, 0.25, 0), + (1, 0.5, 0), + (0, 0.5, 0.5), + (0, 1, 1) +] + +# Config + + +def var(s, v=None): + cfg = app.config + s = 'fm_'+s + return cfg.get(s, v) if v is None else (cfg.__setitem__(s, v), cfg.commit()) + + +def con(v, t): + if var(v) is None: + var(v, t) + + +# Default +con('cwd', getcwd()) +con('star', []) +con('col', COL()) + +# Patches +f = SUB.on_screen_size_change +SUB.on_screen_size_change = lambda *a, **k: (FileMan.resize(), f(*a, **k)) +bw = lambda *a, color=None, textcolor=None, oac=None, bg='white', label='', **k: obw( + *a, + on_activate_call=oac, + texture=gt(bg), + label=label, + enable_sound=False, + color=color or FileMan.COL1, + textcolor=textcolor or FileMan.COL2, + **k +) + +# brobord collide grass +# ba_meta require api 9 +# ba_meta export babase.Plugin + + +class byBordd(Plugin): + def on_app_running(s): + FileMan.loadc() + teck(0.1, s.kang) + + def kang(s): + from bauiv1lib.settings.allsettings import AllSettingsWindow as m + i = '__init__' + o = getattr(m, i) + setattr(m, i, lambda z, *a, **k: (o(z, *a, **k), s.mk(z))[0]) + + def fix(s, p): + m = __import__('logging') + i = 'exception' + o = getattr(m, i) + setattr(m, i, lambda *a, **k: 0 if s.b == p.get_selected_child() else o(*a, **k)) + + def mk(s, z): + s.fix(z._root_widget) + x, y = SCL((1000, 800), (900, 450)) + s.b = obw( + position=(x*0.7, y*SCL(0.5, 0.9)), + parent=z._root_widget, + icon=gt('folder'), + size=(100, 30), + button_type='square', + label='Man', + enable_sound=False, + color=FileMan.COL1, + textcolor=FileMan.COL2, + on_activate_call=lambda: s.run(z) + ) + + def run(s, z): + z.main_window_replace(new_window=FileMan(s.b)) diff --git a/plugins/utilities/finder.py b/plugins/utilities/finder.py new file mode 100644 index 000000000..50d164711 --- /dev/null +++ b/plugins/utilities/finder.py @@ -0,0 +1,508 @@ +# Copyright 2025 - Solely by BrotherBoard +# Intended for personal use only +# Bug? Feedback? Telegram >> GalaxyA14user + +""" +Finder v1.0 - Find anyone + +Experimental. Feedback is appreciated. +Useful if you are looking for someone, or just messing around. + +Features: +- Fetch servers: Pings all servers, then sorts them by lowest +- Ability to cycle through x servers to collect users +- Ability to connect to servers by player name there + +Combine with Power plugin for better control. +""" + +from socket import socket, SOCK_DGRAM +from random import uniform as uf +from babase import Plugin, app +from threading import Thread +from time import time, sleep +from bauiv1 import ( + get_ip_address_type as IPT, + clipboard_set_text as COPY, + get_special_widget as zw, + containerwidget as ocw, + screenmessage as push, + buttonwidget as obw, + scrollwidget as sw, + imagewidget as iw, + textwidget as tw, + gettexture as gt, + apptimer as teck, + getsound as gs, + getmesh as gm, + Call +) +from bascenev1 import ( + disconnect_from_host as BYE, + connect_to_party as CON, + protocol_version as PT, + get_game_roster as GGR +) + + +class Finder: + COL1 = (0, 0.3, 0.3) + COL2 = (0, 0.55, 0.55) + COL3 = (0, 0.7, 0.7) + COL4 = (0, 1, 1) + COL5 = (1, 1, 0) + MAX = 0.3 + TOP = 15 + VER = '1.0' + MEM = [] + BST = [] + SL = None + + def __init__(s, src): + s.thr = [] + s.ikids = [] + s.busy = False + s.s1 = s.snd('powerup01') + c = s.__class__ + # parent + z = (460, 400) + s.p = cw( + scale_origin_stack_offset=src.get_screen_space_center(), + size=z, + oac=s.bye + )[0] + # footing + sw( + parent=s.p, + size=z, + border_opacity=0 + ) + # fetch + tw( + parent=s.p, + text='Fetch Servers', + color=s.COL4, + position=(19, 359) + ) + bw( + parent=s.p, + position=(360, 343), + size=(80, 39), + label='Fetch', + color=s.COL2, + textcolor=s.COL4, + oac=s.fresh + ) + tw( + parent=s.p, + text='Fetches, pings, and sorts public servers.', + color=s.COL3, + scale=0.8, + position=(15, 330), + maxwidth=320 + ) + # separator + iw( + parent=s.p, + size=(429, 1), + position=(17, 330), + texture=gt('white'), + color=s.COL2 + ) + # cycle + tw( + parent=s.p, + text='Cycle Servers', + color=s.COL4, + position=(19, 294) + ) + bw( + parent=s.p, + position=(360, 278), + size=(80, 39), + label='Cycle', + color=s.COL2, + textcolor=s.COL4, + oac=s.find + ) + tw( + parent=s.p, + text='Cycles through best servers and saves their players.', + color=s.COL3, + scale=0.8, + position=(15, 265), + maxwidth=320, + v_align='center' + ) + # separator + iw( + parent=s.p, + size=(429, 1), + position=(17, 265), + texture=gt('white'), + color=s.COL2 + ) + # top + tw( + parent=s.p, + text='Server Cycle Limit', + color=s.COL4, + position=(19, 230) + ) + s.top = tw( + parent=s.p, + position=(398, 228), + size=(80, 50), + text=str(c.TOP), + color=s.COL4, + editable=True, + h_align='center', + v_align='center', + corner_scale=0.1, + scale=10, + allow_clear_button=False, + shadow=0, + flatness=1, + ) + tw( + parent=s.p, + text='Maximum number of servers to cycle.', + color=s.COL3, + scale=0.8, + position=(15, 201), + maxwidth=320 + ) + # separator + iw( + parent=s.p, + size=(429, 1), + position=(17, 200), + texture=gt('white'), + color=s.COL2 + ) + # players + pl = s.plys() + sy = max(len(pl)*30, 140) + p1 = sw( + parent=s.p, + position=(20, 18), + size=(205, 172), + border_opacity=0.4 + ) + p2 = ocw( + parent=p1, + size=(205, sy), + background=False + ) + 0 if pl else tw( + parent=s.p, + position=(90, 100), + text='Cycle some servers\nto collect players', + color=s.COL4, + maxwidth=175, + h_align='center' + ) + s.kids = [] + for _, g in enumerate(pl): + p, a = g + s.kids.append(tw( + parent=p2, + size=(200, 30), + selectable=True, + click_activate=True, + color=s.COL3, + text=p, + position=(0, sy-30-30*_), + maxwidth=175, + on_activate_call=Call(s.hl, _, p), + v_align='center' + )) + # info + iw( + parent=s.p, + position=(235, 18), + size=(205, 172), + texture=gt('scrollWidget'), + mesh_transparent=gm('softEdgeOutside'), + opacity=0.4 + ) + s.tip = tw( + parent=s.p, + position=(310, 98), + text='Select something to\nview server info', + color=s.COL4, + maxwidth=170, + h_align='center' + ) if c.SL is None else 0 + + def hl(s, _, p): + [tw(t, color=s.COL3) for t in s.kids] + tw(s.kids[_], color=s.COL4) + s.info(p) + + def info(s, p): + [_.delete() for _ in s.ikids] + s.ikids.clear() + s.tip and s.tip.delete() + bst = s.__class__.BST + for _ in bst: + for r in _['roster']: + if r['display_string'] == p: + i = _ + break + for _ in range(3): + t = str(i['nap'[_]]) + s.ikids.append(tw( + parent=s.p, + position=(250, 155-40*_), + h_align='center', + v_align='center', + maxwidth=175, + text=t, + color=s.COL4, + size=(175, 30), + selectable=True, + click_activate=True, + on_activate_call=Call(s.copy, t) + )) + s.ikids.append(bw( + parent=s.p, + position=(253, 30), + size=(166, 30), + label='Connect', + color=s.COL2, + textcolor=s.COL4, + oac=Call(CON, i['a'], i['p'], False) + )) + + def copy(s, t): + s.ding(1, 1) + TIP('Copied to clipboard!') + COPY(t) + + def plys(s): + z = [] + me = app.plus.get_v1_account_name() + me = [me, '\ue063'+me] + for _ in s.__class__.BST: + a = _['a'] + if (r := _.get('roster', {})): + for p in r: + ds = p['display_string'] + 0 if ds in me else z.append((ds, a)) + return sorted(z, key=lambda _: _[0].startswith('\ue030Server')) + + def snd(s, t): + l = gs(t) + l.play() + teck(uf(0.14, 0.18), l.stop) + return l + + def bye(s): + s.s1.stop() + ocw(s.p, transition='out_scale') + l = s.snd('laser') + def f(): return teck(0.01, f) if s.p else l.stop() + f() + + def ding(s, i, j): + a = ['Small', ''] + x, y = a[i], a[j] + s.snd('ding'+x) + teck(0.1, gs('ding'+y).play) + + def fresh(s): + if s.busy: + BTW("Still busy!") + return + TIP('Fetching servers...') + s.ding(1, 0) + s.busy = True + p = app.plus + p.add_v1_account_transaction( + { + 'type': 'PUBLIC_PARTY_QUERY', + 'proto': PT(), + 'lang': 'English' + }, + callback=s.kang, + ) + p.run_v1_account_transactions() + + def kang(s, r): + c = s.__class__ + c.MEM = r['l'] + s.thr = [] + for _ in s.__class__.MEM: + t = Thread(target=Call(s.ping, _)) + s.thr.append(t) + t.start() + teck(s.MAX*4, s.join) + + def join(s): + c = s.__class__ + [t.join() for t in s.thr] + far = s.MAX*3000 + c.MEM = [_ for _ in c.MEM if _['ping']] + c.MEM.sort(key=lambda _: _['ping']) + s.thr.clear() + TIP(f'Loaded {len(c.MEM)} servers!') + s.ding(0, 1) + s.busy = False + + def find(s): + if s.busy: + BTW("Still busy!") + return + c = s.__class__ + if not c.MEM: + BTW('Fetch some servers first!') + return + t = tw(query=s.top) + if not t.isdigit(): + BTW('Invalid cycle limit!') + return + top = int(t) + if not (0 < top < len(c.MEM)): + BTW('Cycle count is too '+['big', 'small'][top <= 0]+'!') + return + c.TOP = top + s.ding(1, 0) + TIP('Starting cycle...') + s.busy = True + s.ci = s.lr = 0 + c.BST = c.MEM[:top] + s.cycle() + + def cycle(s): + _ = s.__class__.BST[s.ci] + s.ca = _['a'] + CON(s.ca, _['p'], False) + s.wait() + + def wait(s, i=5): + r = GGR() + if (r != s.lr) and r: + s.__class__.BST[s.ci]['roster'] = s.lr = r + return s.next() + if not i: + s.__class__.BST[s.ci]['roster'] = [] + return s.next() + teck(0.1, Call(s.wait, i-1)) + + def next(s): + s.ci += 1 + if s.ci >= len(s.__class__.BST): + BYE() + teck(0.5, s.yay) + return + s.cycle() + + def yay(s): + TIP('Cycle finished!') + s.ding(0, 1) + s.busy = False + zw('squad_button').activate() + teck(0.3, byBordd.up) + + def ping(s, _): + sock = ping = None + a, p = _['a'], _['p'] + sock = socket(IPT(a), SOCK_DGRAM) + try: + sock.connect((a, p)) + except: + ping = None + else: + st = time() + sock.settimeout(s.MAX) + yes = False + for _i in range(3): + try: + sock.send(b'\x0b') + r = sock.recv(10) + except: + r = None + if r == b'\x0c': + yes = True + break + sleep(s.MAX) + ping = (time()-st)*1000 if yes else None + finally: + _['ping'] = ping + sock.close() + + +# Patches +bw = lambda *, oac=None, **k: obw( + texture=gt('white'), + on_activate_call=oac, + enable_sound=False, + **k +) +cw = lambda *, size=None, oac=None, **k: (p := ocw( + parent=zw('overlay_stack'), + background=False, + transition='in_scale', + size=size, + on_outside_click_call=oac, + **k +)) and (p, iw( + parent=p, + texture=gt('softRect'), + size=(size[0]*1.2, size[1]*1.2), + position=(-size[0]*0.1, -size[1]*0.1), + opacity=0.55, + color=(0, 0, 0) +), iw( + parent=p, + size=size, + texture=gt('white'), + color=Finder.COL1 +)) + +# Global + + +def BTW(t): return (push(t, color=(1, 1, 0)), gs('block').play()) +def TIP(t): return push(t, Finder.COL3) + +# ba_meta require api 9 +# ba_meta export babase.Plugin + + +class byBordd(Plugin): + BTN = None + + @classmethod + def up(c): + c.BTN.activate() if c.BTN.exists() else None + + def __init__(s): + from bauiv1lib import party + p = party.PartyWindow + a = '__init__' + o = getattr(p, a) + setattr(p, a, lambda z, *a, **k: (o(z, *a, **k), s.make(z))[0]) + + def make(s, z): + sz = (80, 30) + p = z._root_widget + x, y = (-60, z._height-45) + iw( + parent=p, + size=(sz[0]*1.34, sz[1]*1.4), + position=(x-sz[0]*0.14, y-sz[1]*0.20), + texture=gt('softRect'), + opacity=0.2, + color=(0, 0, 0) + ) + s.b = s.__class__.BTN = bw( + parent=p, + position=(x, y), + label='Finder', + color=Finder.COL1, + textcolor=Finder.COL3, + size=sz, + oac=lambda: Finder(s.b) + ) diff --git a/plugins/utilities/floater.py b/plugins/utilities/floater.py new file mode 100644 index 000000000..ac8ebe060 --- /dev/null +++ b/plugins/utilities/floater.py @@ -0,0 +1,276 @@ +# Ported by your friend: Freaku + +# Join BCS: +# https://discord.gg/ucyaesh + + +# My GitHub: +# https://github.com/Freaku17/BombSquad-Mods-byFreaku + + +# ba_meta require api 9 +from __future__ import annotations +import babase +import random +import math +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.popuptext import PopupText + + +class Floater(bs.Actor): + def __init__(self, bounds): + super().__init__() + shared = SharedObjects.get() + self.controlled = False + self.source_player = None + self.floaterMaterial = bs.Material() + self.floaterMaterial.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=(('modify_node_collision', 'collide', True), + ('modify_part_collision', 'physical', True))) + self.floaterMaterial.add_actions( + conditions=(('they_have_material', + shared.object_material), 'or', + ('they_have_material', + shared.footing_material), 'or', + ('they_have_material', + self.floaterMaterial)), + actions=('modify_part_collision', 'physical', False)) + + self.pos = bounds + self.px = "random.uniform(self.pos[0],self.pos[3])" + self.py = "random.uniform(self.pos[1],self.pos[4])" + self.pz = "random.uniform(self.pos[2],self.pos[5])" + + self.node = bs.newnode( + 'prop', + delegate=self, + owner=None, + attrs={ + 'position': (eval(self.px), eval(self.py), eval(self.pz)), + 'mesh': + bs.getmesh('landMine'), + 'light_mesh': + bs.getmesh('landMine'), + 'body': + 'landMine', + 'body_scale': + 3, + 'mesh_scale': + 3.1, + 'shadow_size': + 0.25, + 'density': + 999999, + 'gravity_scale': + 0.0, + 'color_texture': + bs.gettexture('achievementFlawlessVictory'), + 'reflection': + 'soft', + 'reflection_scale': [0.25], + 'materials': + [shared.footing_material, self.floaterMaterial] + }) + self.node2 = bs.newnode( + 'prop', + owner=self.node, + attrs={ + 'position': (0, 0, 0), + 'body': + 'sphere', + 'mesh': + None, + 'color_texture': + None, + 'body_scale': + 1.0, + 'reflection': + 'powerup', + 'density': + 999999, + 'reflection_scale': [1.0], + 'mesh_scale': + 1.0, + 'gravity_scale': + 0, + 'shadow_size': + 0.1, + 'is_area_of_interest': + True, + 'materials': + [shared.object_material, self.floaterMaterial] + }) + self.node.connectattr('position', self.node2, 'position') + + def pop(self): PopupText(text="Ported by \ue048Freaku", scale=1.3, position=( + self.node.position[0], self.node.position[1]-1, self.node.position[2]), color=(0, 1, 1)).autoretain() + + def checkCanControl(self): + if not self.node.exists(): + return False + if not self.source_player.is_alive(): + self.dis() + return False + return True + + def con(self): + self.controlled = True + self.checkPlayerDie() + + def up(self): + if not self.checkCanControl(): + return + v = self.node.velocity + self.node.velocity = (v[0], 5, v[2]) + + def upR(self): + if not self.checkCanControl(): + return + v = self.node.velocity + self.node.velocity = (v[0], 0, v[2]) + + def down(self): + if not self.checkCanControl(): + return + v = self.node.velocity + self.node.velocity = (v[0], -5, v[2]) + + def downR(self): + if not self.checkCanControl(): + return + v = self.node.velocity + self.node.velocity = (v[0], 0, v[2]) + + def leftright(self, value): + if not self.checkCanControl(): + return + v = self.node.velocity + self.node.velocity = (5 * value, v[1], v[2]) + + def updown(self, value): + if not self.checkCanControl(): + return + v = self.node.velocity + self.node.velocity = (v[0], v[1], -5 * value) + + def dis(self): + if self.node.exists(): + self.controlled = False + self.node.velocity = (0, 0, 0) + self.move() + + def checkPlayerDie(self): + if not self.controlled: + return + if self.source_player is None: + return + if self.source_player.is_alive(): + bs.timer(1, self.checkPlayerDie) + return + else: + self.dis() + + def distance(self, x1, y1, z1, x2, y2, z2): + d = math.sqrt(math.pow(x2 - x1, 2) + math.pow(y2 - y1, 2) + math.pow(z2 - z1, 2)) + return d + + def drop(self): + try: + np = self.node.position + except: + np = (0, 0, 0) + self.b = Bomb(bomb_type=random.choice(['normal', 'ice', 'sticky', 'impact', 'land_mine', 'tnt']), + source_player=self.source_player, position=(np[0], np[1] - 1, np[2]), velocity=(0, -1, 0)).autoretain() + if self.b.bomb_type in ['impact', 'land_mine']: + self.b.arm() + + def move(self): + px = eval(self.px) + py = eval(self.py) + pz = eval(self.pz) + if self.node.exists() and not self.controlled: + pn = self.node.position + dist = self.distance(pn[0], pn[1], pn[2], px, py, pz) + self.node.velocity = ((px - pn[0]) / dist, (py - pn[1]) / dist, (pz - pn[2]) / dist) + bs.timer(dist-1, bs.WeakCall(self.move)) # suppress_format_warning=True) + + def handlemessage(self, msg): + if isinstance(msg, bs.DieMessage): + self.node.delete() + self.node2.delete() + self.controlled = False + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + else: + super().handlemessage(msg) + + +def assignFloInputs(clientID: int): + activity = bs.get_foreground_host_activity() + with activity.context: + if not hasattr(activity, 'flo') or not activity.flo.node.exists(): + try: + activity.flo = Floater(activity.map.get_def_bound_box('map_bounds')) + except: + return # Perhaps using in main-menu/score-screen + floater = activity.flo + if floater.controlled: + bs.broadcastmessage('Floater is already being controlled', + color=(1, 0, 0), transient=True, clients=[clientID]) + return + bs.broadcastmessage('You Gained Control Over The Floater!\n Press Bomb to Throw Bombs and Punch to leave!', clients=[ + clientID], transient=True, color=(0, 1, 1)) + + for i in activity.players: + if i.sessionplayer.inputdevice.client_id == clientID: + def dis(i, floater): + i.actor.node.invincible = False + i.resetinput() + i.actor.connect_controls_to_player() + floater.dis() + ps = i.actor.node.position + i.actor.node.invincible = True + floater.node.position = (ps[0], ps[1] + 1.0, ps[2]) + bs.timer(1, floater.pop) + i.actor.node.hold_node = bs.Node(None) + i.actor.node.hold_node = floater.node2 + i.actor.connect_controls_to_player() + i.actor.disconnect_controls_from_player() + i.resetinput() + floater.source_player = i + floater.con() + i.assigninput(babase.InputType.PICK_UP_PRESS, floater.up) + i.assigninput(babase.InputType.PICK_UP_RELEASE, floater.upR) + i.assigninput(babase.InputType.JUMP_PRESS, floater.down) + i.assigninput(babase.InputType.JUMP_RELEASE, floater.downR) + i.assigninput(babase.InputType.BOMB_PRESS, floater.drop) + i.assigninput(babase.InputType.PUNCH_PRESS, babase.Call(dis, i, floater)) + i.assigninput(babase.InputType.UP_DOWN, floater.updown) + i.assigninput(babase.InputType.LEFT_RIGHT, floater.leftright) + + +old_fcm = bs.chatmessage + + +def new_chat_message(*args, **kwargs): + old_fcm(*args, **kwargs) + if args[0] == '/floater': + try: + assignFloInputs(-1) + except: + pass + + +bs.chatmessage = new_chat_message + +# ba_meta export babase.Plugin + + +class byFreaku(babase.Plugin): + def on_app_running(self): + pass diff --git a/plugins/utilities/floating_star.py b/plugins/utilities/floating_star.py new file mode 100644 index 000000000..372ecaae6 --- /dev/null +++ b/plugins/utilities/floating_star.py @@ -0,0 +1,172 @@ +# ba_meta require api 9 + +# @BsRush_Mod +# کپی با ذکر منبع آزاد + +from __future__ import annotations +from bascenev1lib.mainmenu import MainMenuSession +from bascenev1._map import Map +import random +import bauiv1 as bui +import bascenev1 as bs +import babase +from typing import TYPE_CHECKING, cast + +plugman = dict( + plugin_name="floating_star", + description="Get floating stars with colorful text", + external_url="", + authors=[ + {"name": "BsRush_Mod", "email": "", "discord": ""}, + ], + version="1.0.0", +) + + +if TYPE_CHECKING: + from typing import Any, Sequence, Callable, List, Dict, Tuple, Optional, Union + +# ==============================================================================# + +# تنظیمات متن +TEXT_CONTENT = "\ue00cBsRush Mod\ue00c" +TEXT_SIZE = 0.01 +TEXT_COLOR = (1, 1, 1) + +# ba_meta export babase.Plugin + + +class UwUuser(babase.Plugin): + Map._old_init = Map.__init__ + + def _new_init(self, vr_overlay_offset: Optional[Sequence[float]] = None) -> None: + self._old_init(vr_overlay_offset) + in_game = not isinstance(bs.get_foreground_host_session(), MainMenuSession) + if not in_game: + return + + def path(): + + shield1 = bs.newnode("shield", attrs={ + 'color': (1, 1, 1), + 'position': (-5.750, 4.3515026107, 2.0), + 'radius': 1.4 + }) + + bs.animate_array(shield1, 'color', 3, { + 0: (random.choice([1, 2, 3, 4, 5, 6, 7, 8, 9]), + random.choice([1, 2, 3, 4, 5, 6, 7, 8, 9]), + random.choice([1, 2, 3, 4, 5, 6, 7, 8, 9])), + 0.2: (2, 0, 2), + 0.4: (2, 2, 0), + 0.6: (0, 2, 0), + 0.8: (0, 2, 2) + }, loop=True) + + flash1 = bs.newnode("flash", attrs={ + 'position': (0, 0, 0), + 'size': 0.6, + 'color': (1, 1, 1) + }) + shield1.connectattr('position', flash1, 'position') + + text_node1 = bs.newnode('text', + attrs={ + 'text': TEXT_CONTENT, + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': TEXT_COLOR, + 'scale': TEXT_SIZE, + 'h_align': 'center' + } + ) + + text_math1 = bs.newnode('math', + attrs={ + 'input1': (0, 1.2, 0), + 'operation': 'add' + } + ) + shield1.connectattr('position', text_math1, 'input2') + text_math1.connectattr('output', text_node1, 'position') + + bs.animate_array(text_node1, 'color', 3, { + 0: (1, 0, 0), # قرمز + 0.2: (1, 1, 0), # زرد + 0.4: (0, 1, 0), # سبز + 0.6: (0, 1, 1), # آبی روشن + 0.8: (1, 0, 1), # بنفش + }, loop=True) + + bs.animate_array(shield1, 'position', 3, { + 0: (-10, 3, -5), + 5: (10, 6, -5), + 10: (-10, 3, 5), + 15: (10, 6, 5), + 20: (-10, 3, -5) + }, loop=True) + + shield2 = bs.newnode("shield", attrs={ + 'color': (1, 1, 1), + 'position': (5.750, 4.3515026107, -2.0), + 'radius': 1.4 + }) + + bs.animate_array(shield2, 'color', 3, { + 0: (random.choice([1, 2, 3, 4, 5, 6, 7, 8, 9]), + random.choice([1, 2, 3, 4, 5, 6, 7, 8, 9]), + random.choice([1, 2, 3, 4, 5, 6, 7, 8, 9])), + 0.2: (0, 2, 2), + 0.4: (2, 0, 2), + 0.6: (2, 2, 0), + 0.8: (0, 2, 0) + }, loop=True) + + flash2 = bs.newnode("flash", attrs={ + 'position': (0, 0, 0), + 'size': 0.6, + 'color': (1, 1, 1) + }) + shield2.connectattr('position', flash2, 'position') + + text_node2 = bs.newnode('text', + attrs={ + 'text': TEXT_CONTENT, + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': TEXT_COLOR, + 'scale': TEXT_SIZE, + 'h_align': 'center' + } + ) + + text_math2 = bs.newnode('math', + attrs={ + 'input1': (0, 1.2, 0), + 'operation': 'add' + } + ) + shield2.connectattr('position', text_math2, 'input2') + text_math2.connectattr('output', text_node2, 'position') + + bs.animate_array(text_node2, 'color', 3, { + 0: (1, 0, 1), # بنفش + 0.2: (0, 1, 1), # آبی روشن + 0.4: (0, 1, 0), # سبز + 0.6: (1, 1, 0), # زرد + 0.8: (1, 0, 0), # قرمز + }, loop=True) + + bs.animate_array(shield2, 'position', 3, { + 0: (10, 6, 5), + 5: (-10, 3, 5), + 10: (10, 6, -5), + 15: (-10, 3, -5), + 20: (10, 6, 5) + }, loop=True) + + bs.timer(0.1, path) + + Map.__init__ = _new_init diff --git a/plugins/utilities/health_indicator.py b/plugins/utilities/health_indicator.py new file mode 100644 index 000000000..bf039c845 --- /dev/null +++ b/plugins/utilities/health_indicator.py @@ -0,0 +1,79 @@ +"""Health Indicator mod v1.2 +Made by Cross Joy +For 1.7.20+""" + +# ---------------------- +# v1.2 update +# Enhance compatibility with other mods. +# Fixed the health is not displaying accurately. +# ---------------------- + +# You can contact me through discord: +# My Discord Id: Cross Joy#0721 +# My BS Discord Server: https://discord.gg/JyBY6haARJ + +# Add a simple health indicator on every player and bot. + +# ba_meta require api 9 + +from __future__ import annotations + +import babase +import bascenev1lib.actor.spaz +from bascenev1lib.actor.spaz import Spaz +import bascenev1 as bs +import bascenev1lib + + +def new_init_spaz_(func): + def wrapper(*args, **kwargs): + func(*args, **kwargs) + m = bs.newnode('math', + owner=args[0].node, + attrs={'input1': (0, 0.7, 0), + 'operation': 'add'}) + args[0].node.connectattr('position', m, 'input2') + args[0]._hitpoint_text = bs.newnode( + 'text', + owner=args[0].node, + attrs={'text': "\ue047" + str(args[0].hitpoints), + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (1, 1, 1), + 'scale': 0.0, + 'h_align': 'center'}) + m.connectattr('output', args[0]._hitpoint_text, 'position') + bs.animate(args[0]._hitpoint_text, 'scale', {0: 0.0, 1.0: 0.01}) + + return wrapper + + +def new_handlemessage_spaz_(func): + def wrapper(*args, **kwargs): + def update_hitpoint_text(spaz): + spaz._hitpoint_text.text = "\ue047" + str(spaz.hitpoints) + r = spaz.hitpoints / 1000 + spaz._hitpoint_text.color = (1, r, r, 1) + + func(*args, **kwargs) + if isinstance(args[1], bs.PowerupMessage): + if args[1].poweruptype == 'health': + update_hitpoint_text(args[0]) + if isinstance( + args[1], bs.HitMessage) or isinstance( + args[1], bs.ImpactDamageMessage): + update_hitpoint_text(args[0]) + + return wrapper + + +# ba_meta export babase.Plugin +class ByCrossJoy(babase.Plugin): + + def __init__(self): + pass + + def on_app_running(self) -> None: + Spaz.__init__ = new_init_spaz_(Spaz.__init__) + Spaz.handlemessage = new_handlemessage_spaz_(Spaz.handlemessage) diff --git a/plugins/utilities/icons_keyboard.py b/plugins/utilities/icons_keyboard.py new file mode 100644 index 000000000..052bb4cf1 --- /dev/null +++ b/plugins/utilities/icons_keyboard.py @@ -0,0 +1,35 @@ +# Made by your friend: Freaku + +# • Icon Keyboard • +# Make your chats look even more cooler! +# Make sure "Always Use Internal Keyboard" is ON +# Double tap the space to change between keyboards... +# Tap bottom-left bomb button to cycle through different icons + + +# ba_meta require api 9 + +import bauiv1 +from babase import SpecialChar +from babase import charstr + +list_of_icons = [i for i in SpecialChar] +list_of_icons = [charstr(i) for i in list_of_icons] +list_of_icons.reverse() + +for i in range(26 - (len(list_of_icons) % 26)): + list_of_icons.append('‎') + + +# ba_meta export bauiv1.Keyboard +class IconKeyboard(bauiv1.Keyboard): + """Keyboard go brrrrrrr""" + name = 'Icons by \ue048Freaku' + chars = [(list_of_icons[0:10]), + (list_of_icons[10:19]), + (list_of_icons[19:26])] + nums = ['‎' for i in range(26)] + pages = { + f'icon{i//26+1}': tuple(list_of_icons[i:i+26]) + for i in range(26, len(list_of_icons), 26) + } diff --git a/plugins/utilities/infinity_shield.py b/plugins/utilities/infinity_shield.py new file mode 100644 index 000000000..0b6863cb0 --- /dev/null +++ b/plugins/utilities/infinity_shield.py @@ -0,0 +1,94 @@ +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +import random +from bascenev1lib.actor.spaz import Spaz +from bascenev1lib.actor.spazfactory import SpazFactory + +if TYPE_CHECKING: + from typing import Sequence + +if TYPE_CHECKING: + pass + + +Spaz._old_init = Spaz.__init__ + + +def __init__(self, + *, + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + character: str = 'Spaz', + source_player: bs.Player = None, + start_invincible: bool = True, + can_accept_powerups: bool = True, + powerups_expire: bool = False, + demo_mode: bool = False): + self._old_init( + color=color, + highlight=highlight, + character=character, + source_player=source_player, + start_invincible=start_invincible, + can_accept_powerups=can_accept_powerups, + powerups_expire=powerups_expire, + demo_mode=demo_mode + ) + if self.source_player: + self.equip_shields() + + def animate_shield(): + if not self.shield: + return + bs.animate_array(self.shield, 'color', 3, { + 0.0: self.shield.color, + 0.2: (random.random(), random.random(), random.random()) + }) + bs.timer(0.2, animate_shield, repeat=True) + self.impact_scale = 0 + + +def equip_shields(self, decay: bool = False) -> None: + """ + Give this spaz a nice energy shield. + """ + + if not self.node: + babase.print_error('Can\'t equip shields; no node.') + return + + factory = SpazFactory.get() + if self.shield is None: + self.shield = bs.newnode('shield', + owner=self.node, + attrs={ + 'color': (0.3, 0.2, 2.0), + 'radius': 1.3 + }) + self.node.connectattr('position_center', self.shield, 'position') + self.shield_hitpoints = self.shield_hitpoints_max = 650 + self.shield_decay_rate = factory.shield_decay_rate if decay else 0 + self.shield.hurt = 0 + factory.shield_up_sound.play(1.0, position=self.node.position) + + if self.impact_scale == 0: + return + + if self.shield_decay_rate > 0: + self.shield_decay_timer = bs.Timer(0.5, + bs.WeakCall(self.shield_decay), + repeat=True) + # So user can see the decay. + self.shield.always_show_health_bar = True + + +# ba_meta export babase.Plugin +class InfinityShieldPlugin(babase.Plugin): + Spaz.__init__ = __init__ + Spaz.equip_shields = equip_shields diff --git a/plugins/utilities/manual_camera.py b/plugins/utilities/manual_camera.py new file mode 100644 index 000000000..a83c4fc7a --- /dev/null +++ b/plugins/utilities/manual_camera.py @@ -0,0 +1,245 @@ +# ba_meta require api 9 + +################### +# Credits - Droopy#3730. # +################### + +# Don't edit . + + +from __future__ import annotations +import _babase +import babase +import bauiv1 as bui +from bauiv1lib.ingamemenu import InGameMenuWindow + + +class Manual_camera_window(bui.MainWindow): + def __init__(self, origin_widget): + super().__init__( + root_widget=bui.containerwidget( + on_outside_click_call=None, + size=(0, 0) + ), + transition='in_scale', + origin_widget=origin_widget, + ) + button_size = (50, 50) + self._text = bui.textwidget(parent=self._root_widget, + scale=0.65, + color=(0.75, 0.75, 0.75), + text='Cam Position', + size=(0, 0), + position=(500, 185), + h_align='center', + v_align='center') + self._xminus = bui.buttonwidget(parent=self._root_widget, + size=button_size, + label=babase.charstr(babase.SpecialChar.LEFT_ARROW), + repeat=True, + button_type='square', + autoselect=True, + position=(429, 60), + on_activate_call=babase.Call(self._change_camera_position, 'x-')) + self._xplus = bui.buttonwidget(parent=self._root_widget, + size=button_size, + label=babase.charstr(babase.SpecialChar.RIGHT_ARROW), + repeat=True, + button_type='square', + autoselect=True, + position=(538, 60), + on_activate_call=babase.Call(self._change_camera_position, 'x')) + self._yplus = bui.buttonwidget(parent=self._root_widget, + size=button_size, + label=babase.charstr(babase.SpecialChar.UP_ARROW), + repeat=True, + button_type='square', + autoselect=True, + position=(482, 120), + on_activate_call=babase.Call(self._change_camera_position, 'y')) + self._yminus = bui.buttonwidget(parent=self._root_widget, + size=button_size, + label=babase.charstr(babase.SpecialChar.DOWN_ARROW), + repeat=True, + button_type='square', + autoselect=True, + position=(482, 2), + on_activate_call=babase.Call(self._change_camera_position, 'y-')) + self.inwards = bui.buttonwidget(parent=self._root_widget, + size=(100, 30), + label='Zoom +', + repeat=True, + button_type='square', + autoselect=True, + position=(-550, -60), + on_activate_call=babase.Call(self._change_camera_position, 'z-')) + self._outwards = bui.buttonwidget(parent=self._root_widget, + size=(100, 30), + label='Zoom -', + repeat=True, + button_type='square', + autoselect=True, + position=(-550, -100), + on_activate_call=babase.Call(self._change_camera_position, 'z')) + self.target_text = bui.textwidget(parent=self._root_widget, + scale=0.65, + color=(0.75, 0.75, 0.75), + text='Cam Angle', + size=(0, 0), + position=(-462, 185), + h_align='center', + v_align='center') + self.target_xminus = bui.buttonwidget(parent=self._root_widget, + size=button_size, + label=babase.charstr(babase.SpecialChar.LEFT_ARROW), + repeat=True, + button_type='square', + autoselect=True, + position=(-538, 60), + on_activate_call=babase.Call(self._change_camera_target, 'x-')) + self.target_xplus = bui.buttonwidget(parent=self._root_widget, + size=button_size, + label=babase.charstr(babase.SpecialChar.RIGHT_ARROW), + repeat=True, + button_type='square', + autoselect=True, + position=(-429, 60), + on_activate_call=babase.Call(self._change_camera_target, 'x')) + self.target_yplus = bui.buttonwidget(parent=self._root_widget, + size=button_size, + label=babase.charstr(babase.SpecialChar.UP_ARROW), + repeat=True, + button_type='square', + autoselect=True, + position=(-482, 120), + on_activate_call=babase.Call(self._change_camera_target, 'y')) + self.target_yminus = bui.buttonwidget(parent=self._root_widget, + size=button_size, + label=babase.charstr(babase.SpecialChar.DOWN_ARROW), + repeat=True, + button_type='square', + autoselect=True, + position=(-482, 2), + on_activate_call=babase.Call(self._change_camera_target, 'y-')) + self._step_text = bui.textwidget(parent=self._root_widget, + scale=0.85, + color=(1, 1, 1), + text='Step:', + size=(0, 0), + position=(450, -38), + h_align='center', + v_align='center') + self._text_field = bui.textwidget( + parent=self._root_widget, + editable=True, + size=(100, 40), + position=(480, -55), + text='', + maxwidth=120, + flatness=1.0, + autoselect=True, + v_align='center', + corner_scale=0.7) + self._reset = bui.buttonwidget(parent=self._root_widget, + size=(50, 30), + label='Reset', + button_type='square', + autoselect=True, + position=(450, -100), + on_activate_call=babase.Call(self._change_camera_position, 'reset')) + self._done = bui.buttonwidget(parent=self._root_widget, + size=(50, 30), + label='Done', + button_type='square', + autoselect=True, + position=(520, -100), + on_activate_call=self.main_window_back) + bui.containerwidget(edit=self._root_widget, + cancel_button=self._done) + + def _change_camera_position(self, direction): + camera = _babase.get_camera_position() + x = camera[0] + y = camera[1] + z = camera[2] + + try: + increment = float(bui.textwidget(query=self._text_field)) + except: + increment = 1 + + if direction == 'x': + x += increment + elif direction == 'x-': + x -= increment + elif direction == 'y': + y += increment + elif direction == 'y-': + y -= increment + elif direction == 'z': + z += increment + elif direction == 'z-': + z -= increment + elif direction == 'reset': + _babase.set_camera_manual(False) + return + + _babase.set_camera_manual(True) + _babase.set_camera_position(x, y, z) + + def _change_camera_target(self, direction): + camera = _babase.get_camera_target() + x = camera[0] + y = camera[1] + z = camera[2] + + try: + increment = float(bui.textwidget(query=self._text_field)) + except: + increment = 1 + + if direction == 'x': + x += increment + elif direction == 'x-': + x -= increment + elif direction == 'y': + y += increment + elif direction == 'y-': + y -= increment + + _babase.set_camera_manual(True) + _babase.set_camera_target(x, y, z) + + +old_refresh_in_game = InGameMenuWindow._refresh_in_game + + +def my_refresh_in_game(self, *args, **kwargs): + value = old_refresh_in_game.__get__(self)(*args, **kwargs) + camera_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(-65, 100), + size=(70, 50), + button_type='square', + label='Manual\nCamera', + text_scale=1.5) + + bui.buttonwidget(edit=camera_button, + on_activate_call=bui.Call(self._manual_camera, camera_button)) + return value + + +def _manual_camera(self, widget): + if not self.main_window_has_control(): + return + + self.main_window_replace(Manual_camera_window(origin_widget=widget)) + +# ba_meta export babase.Plugin + + +class ByDroopy(babase.Plugin): + def __init__(self): + InGameMenuWindow._refresh_in_game = my_refresh_in_game + InGameMenuWindow._manual_camera = _manual_camera diff --git a/plugins/utilities/max_players.py b/plugins/utilities/max_players.py new file mode 100644 index 000000000..3c71fa07c --- /dev/null +++ b/plugins/utilities/max_players.py @@ -0,0 +1,370 @@ +"""===========MAX_PLAYERS===========""" + +# ba_meta require api 9 + +from __future__ import annotations +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +from bascenev1._session import Session +from bascenev1._coopsession import CoopSession, TEAM_COLORS, TEAM_NAMES +from bascenev1._multiteamsession import MultiTeamSession +from bauiv1lib.gather import GatherWindow +from bauiv1lib.popup import PopupWindow + +if TYPE_CHECKING: + from typing import List, Any, Optional, Sequence + + +cfg = babase.app.config +cmp = {'coop_max_players': 4, + 'teams_max_players': 8, + 'ffa_max_players': 8} + +lang = bs.app.lang.language +if lang == 'Spanish': + title_text = 'Máximo de Jugadores' + title_short_text = 'Jugadores' + coop_text = 'Cooperativo' + teams_text = 'Equipos' + ffa_text = 'Todos Contra Todos' +else: + title_text = 'Max Players' + title_short_text = 'Players' + coop_text = 'Co-op' + teams_text = 'Teams' + ffa_text = 'FFA' + + +class ConfigNumberEdit: + + def __init__(self, + parent: bui.Widget, + position: Tuple[float, float], + value: int, + config: str, + text: str): + self._increment = 1 + self._minval = 1 + self._maxval = 100 + self._value = value + self._config = config + + textscale = 1.0 + self.nametext = bui.textwidget( + parent=parent, + position=(position[0], position[1]), + size=(100, 30), + text=text, + maxwidth=150, + color=(0.8, 0.8, 0.8, 1.0), + h_align='left', + v_align='center', + scale=textscale) + self.valuetext = bui.textwidget( + parent=parent, + position=(position[0]+150, position[1]), + size=(60, 28), + editable=False, + color=(0.3, 1.0, 0.3, 1.0), + h_align='right', + v_align='center', + text=str(value), + padding=2) + self.minusbutton = bui.buttonwidget( + parent=parent, + position=(position[0]+240, position[1]), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=babase.Call(self._down), + repeat=True) + self.plusbutton = bui.buttonwidget( + parent=parent, + position=(position[0]+290, position[1]), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=babase.Call(self._up), + repeat=True) + + def _up(self) -> None: + self._value = min(self._maxval, self._value + self._increment) + self._update_display() + + def _down(self) -> None: + self._value = max(self._minval, self._value - self._increment) + self._update_display() + + def _update_display(self) -> None: + bui.textwidget(edit=self.valuetext, text=str(self._value)) + cfg['Config Max Players'][self._config] = self._value + cfg.apply_and_commit() + + +class SettingsMaxPlayers(PopupWindow): + + def __init__(self): + # pylint: disable=too-many-locals + uiscale = bui.app.ui_v1.uiscale + self._transitioning_out = False + self._width = 400 + self._height = 220 + bg_color = (0.5, 0.4, 0.6) + + # creates our _root_widget + PopupWindow.__init__(self, + position=(0.0, 0.0), + size=(self._width, self._height), + scale=1.2, + bg_color=bg_color) + + self._cancel_button = bui.buttonwidget( + parent=self.root_widget, + position=(25, self._height - 40), + size=(50, 50), + scale=0.58, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=bui.gettexture('crossOut'), + iconscale=1.2) + bui.containerwidget(edit=self.root_widget, + cancel_button=self._cancel_button) + + bui.textwidget( + parent=self.root_widget, + position=(self._width * 0.5, self._height - 30), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.8, + text=title_text, + maxwidth=200, + color=bui.app.ui_v1.title_color) + + posx = 33 + posy = self._height + + # co-op + ConfigNumberEdit(parent=self.root_widget, + position=(posx, posy*0.6), + value=cfg['Config Max Players']['coop_max_players'], + config='coop_max_players', + text=coop_text) + + # teams + ConfigNumberEdit(parent=self.root_widget, + position=(posx, posy*0.38), + value=cfg['Config Max Players']['teams_max_players'], + config='teams_max_players', + text=teams_text) + + # ffa + ConfigNumberEdit(parent=self.root_widget, + position=(posx, posy*0.16), + value=cfg['Config Max Players']['ffa_max_players'], + config='ffa_max_players', + text=ffa_text) + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + bui.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + bui.getsound('swish').play() + self._transition_out() + + +def __init__(self) -> None: + """Instantiate a co-op mode session.""" + # pylint: disable=cyclic-import + getcampaign = bui.app.classic.getcampaign + from bascenev1lib.activity.coopjoin import CoopJoinActivity + + _babase.increment_analytics_count('Co-op session start') + app = babase.app + classic = app.classic + + # If they passed in explicit min/max, honor that. + # Otherwise defer to user overrides or defaults. + if 'min_players' in classic.coop_session_args: + min_players = classic.coop_session_args['min_players'] + else: + min_players = 1 + if 'max_players' in classic.coop_session_args: + max_players = classic.coop_session_args['max_players'] + else: + max_players = app.config.get( + 'Coop Game Max Players', + cfg['Config Max Players']['coop_max_players']) + + # print('FIXME: COOP SESSION WOULD CALC DEPS.') + depsets: Sequence[babase.DependencySet] = [] + + Session.__init__(self, + depsets, + team_names=TEAM_NAMES, + team_colors=TEAM_COLORS, + min_players=min_players, + max_players=max_players) + + # Tournament-ID if we correspond to a co-op tournament (otherwise None) + self.tournament_id: Optional[str] = ( + classic.coop_session_args.get('tournament_id')) + + self.campaign = getcampaign(classic.coop_session_args['campaign']) + self.campaign_level_name: str = classic.coop_session_args['level'] + + self._ran_tutorial_activity = False + self._tutorial_activity: Optional[babase.Activity] = None + self._custom_menu_ui: List[Dict[str, Any]] = [] + + # Start our joining screen. + self.setactivity(bs.newactivity(CoopJoinActivity)) + + self._next_game_instance: Optional[bs.GameActivity] = None + self._next_game_level_name: Optional[str] = None + self._update_on_deck_game_instances() + + +def get_max_players(self) -> int: + """Return max number of bs.Players allowed to join the game at once.""" + if self.use_teams: + return _babase.app.config.get( + 'Team Game Max Players', + cfg['Config Max Players']['teams_max_players']) + return _babase.app.config.get( + 'Free-for-All Max Players', + cfg['Config Max Players']['ffa_max_players']) + + +GatherWindow.__old_init__ = GatherWindow.__init__ + + +def __gather_init__(self, + transition: Optional[str] = 'in_right', + origin_widget: bui.Widget = None): + self.__old_init__(transition, origin_widget) + + def _do_max_players(): + SettingsMaxPlayers() + self._max_players_button = bui.buttonwidget( + parent=self._root_widget, + position=(self._width*0.72, self._height*0.91), + size=(220, 60), + scale=1.0, + color=(0.6, 0.0, 0.9), + icon=bui.gettexture('usersButton'), + iconscale=1.5, + autoselect=True, + label=title_short_text, + button_type='regular', + on_activate_call=_do_max_players) + + +def _save_state(self) -> None: + try: + for tab in self._tabs.values(): + tab.save_state() + + sel = self._root_widget.get_selected_child() + selected_tab_ids = [ + tab_id for tab_id, tab in self._tab_row.tabs.items() + if sel == tab.button + ] + if sel == self._back_button: + sel_name = 'Back' + elif sel == self._max_players_button: + sel_name = 'Max Players' + elif selected_tab_ids: + assert len(selected_tab_ids) == 1 + sel_name = f'Tab:{selected_tab_ids[0].value}' + elif sel == self._tab_container: + sel_name = 'TabContainer' + else: + raise ValueError(f'unrecognized selection: \'{sel}\'') + bui.app.ui_v1.window_states[type(self)] = { + 'sel_name': sel_name, + } + except Exception: + babase.print_exception(f'Error saving state for {self}.') + + +def _restore_state(self) -> None: + from efro.util import enum_by_value + try: + for tab in self._tabs.values(): + tab.restore_state() + + sel: Optional[bui.Widget] + winstate = bui.app.ui_v1.window_states.get(type(self), {}) + sel_name = winstate.get('sel_name', None) + assert isinstance(sel_name, (str, type(None))) + current_tab = self.TabID.ABOUT + gather_tab_val = babase.app.config.get('Gather Tab') + try: + stored_tab = enum_by_value(self.TabID, gather_tab_val) + if stored_tab in self._tab_row.tabs: + current_tab = stored_tab + except ValueError: + pass + self._set_tab(current_tab) + if sel_name == 'Back': + sel = self._back_button + elif sel_name == 'Max Players': + sel = self._back_button + elif sel_name == 'TabContainer': + sel = self._tab_container + elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): + try: + sel_tab_id = enum_by_value(self.TabID, + sel_name.split(':')[-1]) + except ValueError: + sel_tab_id = self.TabID.ABOUT + sel = self._tab_row.tabs[sel_tab_id].button + else: + sel = self._tab_row.tabs[current_tab].button + bui.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + babase.print_exception('Error restoring gather-win state.') + +# ba_meta export babase.Plugin + + +class MaxPlayersPlugin(babase.Plugin): + + def has_settings_ui(self) -> bool: + return True + + def show_settings_ui(self, source_widget: bui.Widget | None) -> None: + SettingsMaxPlayers() + + if 'Config Max Players' in babase.app.config: + old_config = babase.app.config['Config Max Players'] + for setting in cmp: + if setting not in old_config: + babase.app.config['Config Max Players'].update({setting: cmp[setting]}) + remove_list = [] + for setting in old_config: + if setting not in cmp: + remove_list.append(setting) + for element in remove_list: + babase.app.config['Config Max Players'].pop(element) + else: + babase.app.config['Config Max Players'] = cmp + babase.app.config.apply_and_commit() + + CoopSession.__init__ = __init__ + MultiTeamSession.get_max_players = get_max_players + GatherWindow.__init__ = __gather_init__ + GatherWindow._save_state = _save_state + GatherWindow._restore_state = _restore_state diff --git a/plugins/utilities/menu_theme.py b/plugins/utilities/menu_theme.py new file mode 100644 index 000000000..6ee35c86e --- /dev/null +++ b/plugins/utilities/menu_theme.py @@ -0,0 +1,2075 @@ +# ba_meta require api 8 +""" +Working for v1.7.20+ only +——————————————————————————————————————— +• Menu Theme v1.0.10 +• discord: riyukiiyan + +I appreciate any kind of modification. So feel free to share, edit and change credit string... no problem +Credits are unnecessary but are a much-appreciated gesture to show support to others :D + +[CHANGELOG]: +~ Support for BombSquad v1.7.26 +~ Fixed "Show Logo Text" checkmark not updating on imports and reset +~ Music changes: + >> Turn off music by selecting 'None' + >> Removed dupes, renamed some and added 2 new musics + +Special thanks to: +snowee, rikko, & unknown +——————————————————————————————————————— +""" +from __future__ import annotations + +from typing import List, Sequence, Callable, Any, cast + +from baenv import TARGET_BALLISTICA_BUILD +from bascenev1lib.mainmenu import MainMenuActivity, NewsDisplay, _preload1 +from bauiv1lib.mainmenu import MainMenuWindow +from bauiv1lib.account.settings import AccountSettingsWindow +from bauiv1lib.colorpicker import ColorPicker, ColorPickerExact +from bauiv1lib.fileselector import FileSelectorWindow +from bauiv1lib.popup import PopupMenuWindow + +import _bauiv1 as _bui +import _bascenev1 as _bs +import _babase as _ba + +import babase as ba +import bascenev1 as bs +import bauiv1 as bui +import bascenev1lib.mainmenu as menu +import json +import os +import shutil +import random +import weakref + + +# defined version and author +__author__ = "Yann" +__version__ = "1.0.10" + +if TARGET_BALLISTICA_BUILD < 21282: + # These attributes have been deprecated as of 1.7.27. For more info see: + # https://github.com/efroemling/ballistica/blob/master/CHANGELOG.md#1727-build-21282-api-8-2023-08-30 + # Adding a compatibility layer here so older builds still work fine. + class Dummy: + pass + babase = ba + babase.app.env = Dummy() + + babase.app.env.build_number = babase.app.build_number + babase.app.env.device_name = babase.app.device_name + babase.app.env.config_file_path = babase.app.config_file_path + babase.app.env.version = babase.app.version + babase.app.env.debug = babase.app.debug_build + babase.app.env.test = babase.app.test_build + babase.app.env.data_directory = babase.app.data_directory + babase.app.env.python_directory_user = babase.app.python_directory_user + babase.app.env.python_directory_app = babase.app.python_directory_app + babase.app.env.python_directory_app_site = babase.app.python_directory_app_site + babase.app.env.api_version = babase.app.api_version + babase.app.env.tv = babase.app.on_tv + babase.app.env.vr = babase.app.vr_mode + babase.app.env.arcade = babase.app.arcade_mode + babase.app.env.headless = babase.app.arcade_mode + babase.app.env.demo = babase.app.demo_mode + protocol_version = babase.app.protocol_version + toolbar_test = babase.app.toolbar_test +else: + protocol_version = _bs.protocol_version + toolbar_test = _bui.toolbar_test() + +# frequently used variables references +config = bs.app.config +ui_type = bs.app.ui_v1.uiscale +ui_small = bs.UIScale.SMALL +ui_medium = bs.UIScale.MEDIUM +ui_large = bs.UIScale.LARGE + +# method references +original_unlocked_pro = bs.app.classic.accounts.have_pro +original_account_init = AccountSettingsWindow.__init__ + +# define globals +GLOBALS_REFLECTION = { + 'Powerup': 'powerup', + 'Character': 'char', + 'Soft': 'soft', + 'None': 'none' +} +GLOBALS_MUSIC = { + 'Menu': bs.MusicType.MENU, + 'Epic': bs.MusicType.EPIC, + 'Flag Catcher': bs.MusicType.FLAG_CATCHER, + 'Flying': bs.MusicType.FLYING, + 'Grand Romp': bs.MusicType.GRAND_ROMP, + 'Lobby': bs.MusicType.CHAR_SELECT, + 'Lobby Epic': bs.MusicType.SCORES, + 'Marching Forward': bs.MusicType.FORWARD_MARCH, + 'Marching Home': bs.MusicType.MARCHING, + 'Run Away': bs.MusicType.RUN_AWAY, + 'Scary': bs.MusicType.SCARY, + 'Sports': bs.MusicType.SPORTS, + 'Survival': bs.MusicType.SURVIVAL, + 'To The Death': bs.MusicType.TO_THE_DEATH, + 'None': None, +} +GLOBALS_MAPDATA = { + "The Pad (with trees)": { + "Camera Bounds": (0.3544110667, 4.493562578, -2.518391331, 16.64754831, 8.06138989, 18.5029888), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.3, + "Ambient": (1.14, 1.1, 1.0), + "Tint": (1.06, 1.04, 1.03), + "Vignette Outer": (0.45, 0.55, 0.54), + "Vignette Inner": (0.99, 0.98, 0.98) + }, + "The Pad": { + "Camera Bounds": (0.3544110667, 4.493562578, -2.518391331, 16.64754831, 8.06138989, 18.5029888), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.3, + "Ambient": (1.1, 1.1, 1.0), + "Tint": (1.1, 1.1, 1.0), + "Vignette Outer": (0.7, 0.65, 0.75), + "Vignette Inner": (0.95, 0.95, 0.93) + }, + "Big G": { + "Camera Bounds": (-0.4011866709, 2.331310176, -0.5426286416, 19.11746262, 10.19675564, 23.50119277), + "Map Color": (0.7, 0.7, 0.7), + "Map Reflection Scale": 0.0, + "Ambient": (1.1, 1.2, 1.3), + "Tint": (1.1, 1.2, 1.3), + "Vignette Outer": (0.65, 0.6, 0.55), + "Vignette Inner": (0.9, 0.9, 0.93) + }, + "Bridgit": { + "Camera Bounds": (-0.2457963347, 3.828181068, -1.528362695, 19.14849937, 7.312788846, 8.436232726), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (1.1, 1.2, 1.3), + "Tint": (1.1, 1.2, 1.3), + "Vignette Outer": (0.65, 0.6, 0.55), + "Vignette Inner": (0.9, 0.9, 0.93) + }, + "Courtyard": { + "Camera Bounds": (0.3544110667, 3.958431362, -2.175025358, 16.37702017, 7.755670126, 13.38680645), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (1.2, 1.17, 1.1), + "Tint": (1.2, 1.17, 1.1), + "Vignette Outer": (0.6, 0.6, 0.64), + "Vignette Inner": (0.95, 0.95, 0.93) + }, + "Crag Castle": { + "Camera Bounds": (0.7033834902, 6.55869393, -3.153439808, 16.73648528, 14.94789935, 11.60063102), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (1.15, 1.05, 0.75), + "Tint": (1.15, 1.05, 0.75), + "Vignette Outer": (0.6, 0.65, 0.6), + "Vignette Inner": (0.95, 0.95, 0.95) + }, + "Doom Shroom": { + "Camera Bounds": (0.4687647786, 2.320345088, -3.219423694, 21.34898078, 10.25529817, 14.67298352), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (0.9, 1.3, 1.1), + "Tint": (0.82, 1.10, 1.15), + "Vignette Outer": (0.76, 0.76, 0.76), + "Vignette Inner": (0.95, 0.95, 0.99) + }, + "Happy Thoughts": { + "Camera Bounds": (-1.045859963, 12.67722855, -5.401537075, 34.46156851, 20.94044653, 0.6931564611), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (1.3, 1.23, 1.0), + "Tint": (1.3, 1.23, 1.0), + "Vignette Outer": (0.64, 0.59, 0.69), + "Vignette Inner": (0.95, 0.95, 0.93) + }, + "Football Stadium": { + "Camera Bounds": (0.0, 1.185751251, 0.4326226188, 29.8180273, 11.57249038, 18.89134176), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (1.3, 1.2, 1.0), + "Tint": (1.3, 1.2, 1.0), + "Vignette Outer": (0.57, 0.57, 0.57), + "Vignette Inner": (0.9, 0.9, 0.9) + }, + "Hockey Stadium": { + "Camera Bounds": (0.0, 0.7956858119, 0.0, 30.80223883, 0.5961646365, 13.88431707), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.3, + "Ambient": (1.15, 1.25, 1.6), + "Tint": (1.2, 1.3, 1.33), + "Vignette Outer": (0.66, 0.67, 0.73), + "Vignette Inner": (0.93, 0.93, 0.95) + }, + "Lake Frigid": { + "Camera Bounds": (0.622753268, 3.958431362, -2.48708008, 20.62310543, 7.755670126, 12.33155049), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (1, 1, 1), + "Tint": (1, 1, 1), + "Vignette Outer": (0.86, 0.86, 0.86), + "Vignette Inner": (0.95, 0.95, 0.99) + }, + "Monkey Face": { + "Camera Bounds": (-1.657177611, 4.132574186, -1.580485661, 17.36258946, 10.49020453, 12.31460338), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (1.1, 1.2, 1.2), + "Tint": (1.1, 1.2, 1.2), + "Vignette Outer": (0.60, 0.62, 0.66), + "Vignette Inner": (0.97, 0.95, 0.93) + }, + "Rampage": { + "Camera Bounds": (0.3544110667, 5.616383286, -4.066055072, 19.90053969, 10.34051135, 8.16221072), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (1.3, 1.2, 1.03), + "Tint": (1.2, 1.1, 0.97), + "Vignette Outer": (0.62, 0.64, 0.69), + "Vignette Inner": (0.97, 0.95, 0.93) + }, + "Roundabout": { + "Camera Bounds": (-1.552280404, 3.189001207, -2.40908495, 11.96255385, 8.857531648, 9.531689995), + "Map Color": (0.7, 0.7, 0.7), + "Map Reflection Scale": 0.0, + "Ambient": (1.0, 1.05, 1.1), + "Tint": (1.0, 1.05, 1.1), + "Vignette Outer": (0.63, 0.65, 0.7), + "Vignette Inner": (0.97, 0.95, 0.93) + }, + "Step Right Up": { + "Camera Bounds": (0.3544110667, 6.07676405, -2.271833016, 22.55121262, 10.14644532, 14.66087273), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (1.2, 1.1, 1.0), + "Tint": (1.2, 1.1, 1.0), + "Vignette Outer": (1.2, 1.1, 1.0), + "Vignette Inner": (0.95, 0.95, 0.93) + }, + "Tower D": { + "Camera Bounds": (-0.4714933293, 2.887077774, -1.505479919, 17.90145968, 6.188484831, 15.96149117), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (1.2, 1.1, 1.0), + "Tint": (1.15, 1.11, 1.03), + "Vignette Outer": (1.2, 1.1, 1.0), + "Vignette Inner": (0.95, 0.95, 0.95) + }, + "Tip Top": { + "Camera Bounds": (0.004375512593, 7.141135803, -0.01745294675, 21.12506141, 4.959977313, 16.6885592), + "Map Color": (0.7, 0.7, 0.7), + "Map Reflection Scale": 0.0, + "Ambient": (0.8, 0.9, 1.3), + "Tint": (0.8, 0.9, 1.3), + "Vignette Outer": (0.79, 0.79, 0.69), + "Vignette Inner": (0.97, 0.97, 0.99) + }, + "Zig Zag": { + "Camera Bounds": (-1.807378035, 3.943412768, -1.61304303, 23.01413538, 13.27980464, 10.0098376), + "Map Color": (1.0, 1.0, 1.0), + "Map Reflection Scale": 0.0, + "Ambient": (1.0, 1.15, 1.15), + "Tint": (1.0, 1.15, 1.15), + "Vignette Outer": (0.57, 0.59, 0.63), + "Vignette Inner": (0.97, 0.95, 0.93) + } +} + + +class CustomColorPicker(ColorPicker): + + def _select_other(self): + from bauiv1lib import purchase + + CustomColorPickerExact(parent=self._parent, + position=self._position, + initial_color=self._initial_color, + delegate=self._delegate, + scale=self._scale, + offset=self._offset, + tag=self._tag) + self._delegate = None + self._transition_out() + + +class CustomColorPickerExact(ColorPickerExact): + + def _color_change_press(self, color_name: str, increasing: bool): + current_time = bui.apptime() + since_last = current_time - self._last_press_time + if ( + since_last < 0.2 + and self._last_press_color_name == color_name + and self._last_press_increasing == increasing + ): + self._change_speed += 0.25 + else: + self._change_speed = 1.0 + self._last_press_time = current_time + self._last_press_color_name = color_name + self._last_press_increasing = increasing + + color_index = ('r', 'g', 'b').index(color_name) + offs = int(self._change_speed) * (0.01 if increasing else -0.01) + self._color[color_index] = max( + -1.0, min(2.55, self._color[color_index] + offs) + ) + self._update_for_color() + + +class ConfigCheckBox: + widget: bui.Widget + + def __init__( + self, + parent: bui.Widget, + configkey: str, + position: tuple[float, float], + size: tuple[float, float], + displayname: str | bs.Lstr | None = None, + scale: float | None = None, + maxwidth: float | None = None, + autoselect: bool = True, + value_change_call: Callable[[Any], Any] | None = None): + + if displayname is None: + displayname = configkey + self._value_change_call = value_change_call + self._configkey = configkey + self.widget = bui.checkboxwidget( + parent=parent, + autoselect=autoselect, + position=position, + size=size, + text=displayname, + textcolor=(0.8, 0.8, 0.8), + value=bs.app.config[configkey], + on_value_change_call=self._value_changed, + scale=scale, + maxwidth=maxwidth, + ) + # complain if we outlive our checkbox + bui.uicleanupcheck(self, self.widget) + + def _value_changed(self, val: bool): + cfg = bs.app.config + cfg[self._configkey] = val + if self._value_change_call is not None: + self._value_change_call(val) + cfg.apply_and_commit() + + +class FreeEditWindow(bui.Window): + + def _do_enter(self): + + def _error() -> None: + bui.getsound('error').play(volume=2.0) + bui.screenmessage('error ' + u'😑😑', color=(1.0, 0.0, 0.0)) + + try: + if self.name_only: + value = bui.textwidget(query=self._text_field) + if not value.strip(): + return _error() + + self.delegate._export(self, txt=value) + else: + value = round(float(bui.textwidget(query=self._text_field)), 4) + self.delegate.free_edit_enter(self, c=self.config_name, txt=value) + + except ValueError: + return _error() + bui.containerwidget(edit=self._root_widget, transition=self._transition_out) + + def _activate_enter_button(self): + self._enter_button.activate() + + def _do_back(self): + bui.containerwidget(edit=self._root_widget, transition=self._transition_out) + + def __init__(self, delegate: Any = None, config_name: str = 'Menu Map', whitelist: List = [], name_only: bool = False, origin_widget: bui.widget = None): + self._transition_out = 'out_scale' if origin_widget else 'out_right' + scale_origin = origin_widget.get_screen_space_center() if origin_widget else None + transition = 'in_scale' if origin_widget else 'in_right' + width, height = 450, 230 + uiscale = bs.app.ui_v1.uiscale + + super().__init__(root_widget=bui.containerwidget( + size=(width, height), + transition=transition, + toolbar_visibility='menu_minimal_no_back', + scale_origin_stack_offset=scale_origin, + scale=(2.0 if uiscale is bs.UIScale.SMALL else + 1.5 if uiscale is bs.UIScale.MEDIUM else 1.0))) + + btn = bui.buttonwidget(parent=self._root_widget, + scale=0.5, + position=(40, height - 40), + size=(60, 60), + label='', + on_activate_call=self._do_back, + autoselect=True, + color=(0.55, 0.5, 0.6), + icon=bui.gettexture('crossOut'), + iconscale=1.2) + + self.config_name, self.delegate, self.name_only = config_name, delegate, name_only + + self._text_field = bui.textwidget( + parent=self._root_widget, + position=(125, height - 121), + size=(280, 46), + text='', + h_align='left', + v_align='center', + color=(0.9, 0.9, 0.9, 1.0), + description='', + editable=True, + padding=4, + max_chars=20 if name_only else 5, + on_return_press_call=self._activate_enter_button) + + bui.textwidget(parent=self._root_widget, + text='Current: ' + str(config[config_name]) if not name_only else 'Save as', + position=(220, height - 44), + color=(0.5, 0.5, 0.5, 1.0), + size=(90, 30), + h_align='right') + + bui.widget(edit=btn, down_widget=self._text_field) + + b_width = 200 + self._enter_button = btn2 = bui.buttonwidget( + parent=self._root_widget, + position=(width * 0.5 - b_width * 0.5, height - 200), + size=(b_width, 60), + scale=1.0, + label='Enter', + on_activate_call=self._do_enter) + bui.containerwidget(edit=self._root_widget, + cancel_button=btn, + start_button=btn2, + selected_child=self._text_field) + + +class MenuThemeWindow: + def __init__(self, origin_widget: bui.widget = None, accounts_window=None): + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + transition = 'in_right' + + self._choice_page: str = None + self._accounts_window = accounts_window + height = 500 if ui_type is ui_small else 772 + + self._root_widget = bui.containerwidget( + size=(445, 365) if ui_type is ui_small else (799, 576), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + scale=2.23 if ui_type is ui_small else 1.0, + stack_offset=(0, -35) if ui_type is ui_small else (0, 0) + ) + + self._scroll_border_parent = bui.scrollwidget( + parent=self._root_widget, + position=(39, 58) if ui_type is ui_small else (86, 39), + size=(375, 240) if ui_type is ui_small else (645, 463), + color=(0.52, 0.48, 0.63) + ) + + self._scroll_parent = bui.containerwidget( + parent=self._scroll_border_parent, + size=(450, height), + background=False, + claims_left_right=False, + claims_tab=False + ) + + self._back_button = bui.buttonwidget( + parent=self._root_widget, + position=(23, 310) if ui_type is ui_small else (80, 511), + size=(35, 35) if ui_type is ui_small else (55, 55), + color=(0.76, 0.42, 0.38), + button_type="backSmall", + textcolor=(1, 1, 1), + text_scale=0.8 if ui_type is ui_small else 1.0, + label="", + autoselect=True, + on_activate_call=bs.Call(self.close) + ) + + self._home_button = bui.buttonwidget( + parent=self._root_widget, + position=(383, 308) if ui_type is ui_small else (688, 511), + size=(60, 60) if ui_type is ui_small else (55, 55), + color=(0.76, 0.42, 0.38), + icon=bui.gettexture('crossOut'), + iconscale=1.2, + scale=0.59 if ui_type is ui_small else 0.92, + label="", + autoselect=True, + on_activate_call=bs.Call(self.close_all) + ) + + # menutheme title + bui.textwidget( + parent=self._root_widget, + position=(225, 327) if ui_type is ui_small else (415, 547), + size=(0, 0), + text="Menu Theme", + color=(0.8, 0.8, 0.8, 0.76), + maxwidth=290, + scale=0.95 if ui_type is ui_small else 1.25, + h_align='center', + v_align='center' + ) + + bui.textwidget( + parent=self._root_widget, + position=(225, 309) if ui_type is ui_small else (415, 517), + size=(0, 0), + text=f"version: {__version__}", + color=(0.6, 0.6, 0.6, 0.8), + maxwidth=290, + scale=0.454 if ui_type is ui_small else 0.653, + h_align='center', + v_align='center' + ) + + # settings txt + bui.textwidget( + parent=self._scroll_parent, + position=(10, height - 27) if ui_type is ui_small else (30, height - 47), + size=(0, 0), + text="Map Type:", + color=(1, 1, 1, 0.8), + maxwidth=290, + scale=0.7 if ui_type is ui_small else 1.3, + h_align='left', + v_align='center' + ) + + bui.textwidget( + parent=self._scroll_parent, + position=(10, height - 65) if ui_type is ui_small else (30, height - 104), + size=(0, 0), + text="Music:", + color=(1, 1, 1, 0.8), + maxwidth=290, + scale=0.7 if ui_type is ui_small else 1.3, + h_align='left', + v_align='center' + ) + + bui.textwidget( + parent=self._scroll_parent, + position=(25, height - 147) if ui_type is ui_small else (45, height - 239), + size=(0, 0), + text="tint", + color=(1, 1, 1, 0.8), + maxwidth=290, + scale=0.5 if ui_type is ui_small else 1.0, + h_align='left', + v_align='center' + ) + + bui.textwidget( + parent=self._scroll_parent, + position=(81, height - 147) if ui_type is ui_small else (140, height - 239), + size=(0, 0), + text="ambient", + color=(1, 1, 1, 0.8), + maxwidth=290, + scale=0.5 if ui_type is ui_small else 1.0, + h_align='left', + v_align='center' + ) + + bui.textwidget( + parent=self._scroll_parent, + position=(147, height - 156) if ui_type is ui_small else (262, height - 258), + size=(0, 0), + text="vignette\n outer", + color=(1, 1, 1, 0.8), + maxwidth=290, + scale=0.5 if ui_type is ui_small else 1.0, + h_align='left', + v_align='center' + ) + + bui.textwidget( + parent=self._scroll_parent, + position=(213, height - 156) if ui_type is ui_small else (382, height - 258), + size=(0, 0), + text="vignette\n inner", + color=(1, 1, 1, 0.8), + maxwidth=290, + scale=0.5 if ui_type is ui_small else 1.0, + h_align='left', + v_align='center' + ) + + bui.textwidget( + parent=self._scroll_parent, + position=(279, height - 156) if ui_type is ui_small else (500, height - 258), + size=(0, 0), + text="reflection\n color", + color=(1, 1, 1, 0.8), + maxwidth=290, + scale=0.5 if ui_type is ui_small else 1.0, + h_align='left', + v_align='center' + ) + + bui.textwidget( + parent=self._scroll_parent, + position=(10, height - 193) if ui_type is ui_small else (30, height - 320), + size=(0, 0), + text="Reflection Type:", + color=(1, 1, 1, 0.8), + maxwidth=290, + scale=0.7 if ui_type is ui_small else 1.3, + h_align='left', + v_align='center' + ) + + bui.textwidget( + parent=self._scroll_parent, + position=(10, height - 227) if ui_type is ui_small else (30, height - 373), + size=(0, 0), + text="Reflection Scale:", + color=(1, 1, 1, 0.8), + maxwidth=290, + scale=0.7 if ui_type is ui_small else 1.3, + h_align='left', + v_align='center' + ) + + bui.textwidget( + parent=self._scroll_parent, + position=(10, height - 260) if ui_type is ui_small else (30, height - 423), + size=(0, 0), + text="Camera Mode:", + color=(1, 1, 1, 0.8), + maxwidth=290, + scale=0.7 if ui_type is ui_small else 1.3, + h_align='left', + v_align='center' + ) + + bui.textwidget( + parent=self._scroll_parent, + position=(10, height - 294) if ui_type is ui_small else (30, height - 480), + size=(0, 0), + text="Show Logo Text:", + color=(1, 1, 1, 0.8), + maxwidth=290, + scale=0.7 if ui_type is ui_small else 1.3, + h_align='left', + v_align='center' + ) + + # prioritize this first for: + # >> handling config-errors + # >> debugging + self._menu_configreset_button = bui.buttonwidget( + parent=self._scroll_parent, + position=(5, height - 486) if ui_type is ui_small else (12, height - 765), + size=(329, 50) if ui_type is ui_small else (600, 80), + color=(0.0, 0.67, 0.85), + textcolor=(0.8, 0.8, 0.8), + button_type="regular", + label="Reset to Default Settings", + text_scale=0.7 if ui_type is ui_small else 1.0, + on_activate_call=bs.Call(self.reset_config) + ) + + # settings buttons + self._menu_map_button = bui.buttonwidget( + parent=self._scroll_parent, + position=(112, height - 38) if ui_type is ui_small else (206, height - 67), + size=(87, 24) if ui_type is ui_small else (149, 40), + button_type="regular", + label=config["Menu Map"], + on_activate_call=bs.Call(self.choice_window, 'Map') + ) + + self._menu_music_button = bui.buttonwidget( + parent=self._scroll_parent, + position=(85, height - 75) if ui_type is ui_small else (149, height - 123), + size=(87, 24) if ui_type is ui_small else (149, 40), + button_type="regular", + label=config["Menu Music"], + text_scale=0.6 if ui_type is ui_small else 1.0, + on_activate_call=bs.Call(self.choice_window, 'Music') + ) + + self._menu_tint_button = bui.buttonwidget( + parent=self._scroll_parent, + color=config["Menu Tint"], + position=(15, height - 136) if ui_type is ui_small else (30, height - 220), + size=(40, 40) if ui_type is ui_small else (70, 70), + button_type="square", + label="", + on_activate_call=bs.WeakCall(self.color_picker_popup, "Menu Tint") + ) + + self._menu_ambient_button = bui.buttonwidget( + parent=self._scroll_parent, + color=config["Menu Ambient"], + position=(81, height - 136) if ui_type is ui_small else (150, height - 220), + size=(40, 40) if ui_type is ui_small else (70, 70), + button_type="square", + label="", + on_activate_call=bs.WeakCall(self.color_picker_popup, "Menu Ambient") + ) + + self._menu_vignetteO_button = bui.buttonwidget( + parent=self._scroll_parent, + color=config["Menu Vignette Outer"], + position=(147, height - 136) if ui_type is ui_small else (270, height - 220), + size=(40, 40) if ui_type is ui_small else (70, 70), + button_type="square", + label="", + on_activate_call=bs.WeakCall(self.color_picker_popup, "Menu Vignette Outer") + ) + + self._menu_vignetteI_button = bui.buttonwidget( + parent=self._scroll_parent, + color=config["Menu Vignette Inner"], + position=(213, height - 136) if ui_type is ui_small else (390, height - 220), + size=(40, 40) if ui_type is ui_small else (70, 70), + button_type="square", + label="", + on_activate_call=bs.WeakCall(self.color_picker_popup, "Menu Vignette Inner") + ) + + self._menu_rcolor_button = bui.buttonwidget( + parent=self._scroll_parent, + color=config["Menu Map Color"], + position=(279, height - 136) if ui_type is ui_small else (510, height - 220), + size=(40, 40) if ui_type is ui_small else (70, 70), + button_type="square", + label="", + on_activate_call=bs.WeakCall(self.color_picker_popup, "Menu Map Color") + ) + + self._menu_reflectiont_button = bui.buttonwidget( + parent=self._scroll_parent, + position=(148, height - 204) if ui_type is ui_small else (287, height - 339), + size=(87, 24) if ui_type is ui_small else (149, 40), + button_type="regular", + label=config["Menu Reflection Type"], + text_scale=0.6 if ui_type is ui_small else 1.0, + on_activate_call=bs.Call(self.choice_window, 'Reflection Type') + ) + + self._menu_reflections_button = bui.buttonwidget( + parent=self._scroll_parent, + position=(153, height - 237) if ui_type is ui_small else (289, height - 392), + size=(87, 24) if ui_type is ui_small else (149, 40), + button_type="regular", + label=str(config["Menu Reflection Scale"]), + text_scale=0.6 if ui_type is ui_small else 1.0, + on_activate_call=lambda: FreeEditWindow( + delegate=self, whitelist=['num'], config_name='Menu Reflection Scale') + ) + + self._menu_cameramode_button = bui.buttonwidget( + parent=self._scroll_parent, + position=(138, height - 272) if ui_type is ui_small else (265, height - 444), + size=(87, 24) if ui_type is ui_small else (149, 40), + button_type="regular", + label=str(config["Menu Camera Mode"]), + text_scale=0.6 if ui_type is ui_small else 1.0, + on_activate_call=bs.Call(self.choice_window, 'Camera Mode') + ) + + self._menu_logotext_button = ConfigCheckBox( + parent=self._scroll_parent, + configkey="Menu Logo Text", + position=(151, height - 308) if ui_type is ui_small else (287, height - 520), + size=(40, 40) if ui_type is ui_small else (56, 56), + scale=0.62 if ui_type is ui_small else 1.4, + displayname="" + ) + + self._menu_load_button = bui.buttonwidget( + parent=self._scroll_parent, + position=(11, height - 365) if ui_type is ui_small else (22, height - 590), + size=(155, 45) if ui_type is ui_small else (280, 75), + textcolor=(0.8, 0.8, 0.8), + button_type="regular", + label="Load Theme", + text_scale=0.7 if ui_type is ui_small else 1.0, + on_activate_call=bs.Call(self.popup_fileselector) + ) + + self._menu_save_button = bui.buttonwidget( + parent=self._scroll_parent, + position=(178, height - 365) if ui_type is ui_small else (312, height - 590), + size=(155, 45) if ui_type is ui_small else (280, 75), + textcolor=(0.8, 0.8, 0.8), + button_type="regular", + label="Save Theme", + text_scale=0.7 if ui_type is ui_small else 1.0, + on_activate_call=lambda: FreeEditWindow(delegate=self, name_only=True) + ) + + self._menu_mapdata_button = bui.buttonwidget( + parent=self._scroll_parent, + position=(5, height - 425) if ui_type is ui_small else (12, height - 677), + size=(329, 50) if ui_type is ui_small else (600, 80), + color=(0.23, 0.27, 0.55), + textcolor=(0.8, 0.8, 0.8), + button_type="regular", + label="Map Data Overrides", + text_scale=0.7 if ui_type is ui_small else 1.0, + on_activate_call=bs.Call(self.checkbox_window) + ) + + def checkbox_window(self): + self._root_widget_checkbox = bui.containerwidget( + size=(800, 740), + transition='in_scale', + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=(0, 0), + scale=0.6 if ui_type is ui_large else 0.88 if ui_type is ui_small else 0.76, + color=(0.17, 0.2, 0.25), + on_outside_click_call=bs.Call(self.checkbox_window_out), + claim_outside_clicks=True, + stack_offset=(0, -35) if ui_type is ui_small else (0, 0) + ) + self._button_tint = ConfigCheckBox( + parent=self._root_widget_checkbox, + configkey="tint", + position=(88, 600), + size=(380, 40), + scale=1.9, + displayname='Tint' + ) + self._button_ambient = ConfigCheckBox( + parent=self._root_widget_checkbox, + configkey="ambient_color", + position=(88, 510), + size=(380, 40), + scale=1.9, + displayname='Ambient Color' + ) + self._button_vignette_outer = ConfigCheckBox( + parent=self._root_widget_checkbox, + configkey="vignette_outer", + position=(88, 420), + size=(380, 40), + scale=1.9, + displayname='Vignette Outer' + ) + self._button_vignette_inner = ConfigCheckBox( + parent=self._root_widget_checkbox, + configkey="vignette_inner", + position=(88, 330), + size=(380, 40), + scale=1.9, + displayname='Vignette Inner' + ) + self._button_map_color = ConfigCheckBox( + parent=self._root_widget_checkbox, + configkey="map_color", + position=(88, 240), + size=(380, 40), + scale=1.9, + displayname='Map Color' + ) + self._button_map_reflection_scale = ConfigCheckBox( + parent=self._root_widget_checkbox, + configkey="map_reflection_scale", + position=(88, 150), + size=(380, 40), + scale=1.9, + displayname='Map Reflection Scale' + ) + self._button_map_reflection_type = ConfigCheckBox( + parent=self._root_widget_checkbox, + configkey="map_reflection_type", + position=(88, 60), + size=(380, 40), + scale=1.9, + displayname='Map Reflection Type' + ) + + def checkbox_window_out(self): + # Memory-leak prevention + bui.containerwidget(edit=self._root_widget_checkbox, transition='out_scale') + del self._button_tint + del self._button_ambient + del self._button_vignette_outer + del self._button_vignette_inner + del self._button_map_color + del self._button_map_reflection_scale + del self._button_map_reflection_type + + def choice_window(self, category: str): + choices_map = { + 'Map': [ + 'Big G', + 'Bridgit', + 'Courtyard', + 'Crag Castle', + 'Doom Shroom', + 'Football Stadium', + 'Happy Thoughts', + 'Hockey Stadium', + 'Lake Frigid', + 'Monkey Face', + 'Rampage', + 'Roundabout', + 'Step Right Up', + 'The Pad', + 'The Pad (with trees)', + 'Tower D', + 'Tip Top', + 'Zig Zag' + ], + 'Camera Mode': [ + 'rotate', + 'static' + ], + 'Reflection Type': [ + 'Soft', + 'None', + 'Powerup', + 'Character' + ], + 'Music': [ + 'Menu', + 'Epic', + 'Flag Catcher', + 'Flying', + 'Grand Romp', + 'Lobby', + 'Lobby Epic', + 'Marching Forward', + 'Marching Home', + 'Run Away', + 'Scary', + 'Sports', + 'Survival', + 'To The Death', + 'None' + ] + } + + if category in choices_map: + PopupMenuWindow( + position=(0, 0), + scale=2.0 if ui_type is ui_small else 1.0, + delegate=self, + current_choice=bs.app.config[f"Menu {category}"], + choices=choices_map[category] + ) + self._choice_page = category + + def popup_menu_selected_choice(self, window: PopupMenuWindow, choice: str): + if self._choice_page == 'Map': + bs.app.config['Menu Map'] = choice + if config["tint"]: + bs.app.config["Menu Tint"] = GLOBALS_MAPDATA.get(config["Menu Map"]).get("Tint") + if config["ambient_color"]: + bs.app.config["Menu Ambient"] = GLOBALS_MAPDATA.get( + config["Menu Map"]).get("Ambient") + if config["vignette_outer"]: + bs.app.config["Menu Vignette Outer"] = GLOBALS_MAPDATA.get( + config["Menu Map"]).get("Vignette Outer") + if config["vignette_inner"]: + bs.app.config["Menu Vignette Inner"] = GLOBALS_MAPDATA.get( + config["Menu Map"]).get("Vignette Inner") + if config["map_color"]: + bs.app.config["Menu Map Color"] = GLOBALS_MAPDATA.get( + config["Menu Map"]).get("Map Color") + if config["map_reflection_scale"]: + bs.app.config["Menu Reflection Scale"] = GLOBALS_MAPDATA.get( + config["Menu Map"]).get("Map Reflection Scale") + if config["map_reflection_type"]: + bs.app.config["Menu Reflection Type"] = 'Soft' + + elif self._choice_page == 'Music': + bs.app.config['Menu Music'] = choice + + elif self._choice_page == 'Reflection Type': + bs.app.config['Menu Reflection Type'] = choice + + elif self._choice_page == 'Camera Mode': + bs.app.config['Menu Camera Mode'] = choice + bs.app.config.apply_and_commit() + self.update_buttons() + + def popup_menu_closing(self, window: PopupMenuWindow): + self._choice_page = None + self.update_buttons() + + def popup_fileselector(self): + self._file_selector = FileSelectorWindow( + path=str(_ba.env()['python_directory_user']), + callback=self._import, + show_base_path=True, + valid_file_extensions=['json'], + allow_folders=False + ) + + def _import(self, path: str = None): + try: + self.path = path + '/' + self.path = self.path[:-1] + with open(self.path, 'r') as imported: + selected = json.load(imported) + handle_config([ + selected["Menu Map"], + selected["Menu Tint"], + selected["Menu Ambient"], + selected["Menu Vignette Outer"], + selected["Menu Vignette Inner"], + selected["Menu Music"], + selected["Menu Map Color"], + selected["Menu Reflection Scale"], + selected["Menu Reflection Type"], + selected["Menu Camera Mode"], + selected["Menu Logo Text"], + selected["vignette_outer"], + selected["vignette_inner"], + selected["ambient_color"], + selected["tint"], + selected["map_reflection_scale"], + selected["map_reflection_type"], + selected["map_color"]], + False + ) + self.update_buttons() + bui.screenmessage( + f"Loaded {os.path.splitext(os.path.basename(self.path))[0]}!", color=(0.2, 0.4, 1.0)) + except: + pass + del self._file_selector + + def _export(self, window: FreeEditWindow, txt: Any): + path = _ba.env()['python_directory_user'] + "/_menutheme/" + try: + a = _ba.env()['python_directory_user'] + "/_menutheme" + os.makedirs(a, exist_ok=False) + except: + pass + + with open(path + txt + '.json', 'w') as file: + my_config = { + "Menu Map": config["Menu Map"], + "Menu Tint": config["Menu Tint"], + "Menu Ambient": config["Menu Ambient"], + "Menu Vignette Outer": config["Menu Vignette Outer"], + "Menu Vignette Inner": config["Menu Vignette Inner"], + "Menu Music": config["Menu Music"], + "Menu Map Color": config["Menu Map Color"], + "Menu Reflection Scale": config["Menu Reflection Scale"], + "Menu Reflection Type": config["Menu Reflection Type"], + "Menu Camera Mode": config["Menu Camera Mode"], + "Menu Logo Text": config["Menu Logo Text"], + "vignette_outer": config["vignette_outer"], + "vignette_inner": config["vignette_inner"], + "ambient_color": config["ambient_color"], + "tint": config["tint"], + "map_color": config["map_color"], + "map_reflection_scale": config["map_reflection_scale"], + "map_reflection_type": config["map_reflection_type"] + } + json.dump(my_config, file, indent=4) + bui.screenmessage( + f"Saved {os.path.splitext(os.path.basename(path+txt+'.json'))[0]}!", color=(0.2, 0.4, 1.0)) + bui.getsound('gunCocking').play() + + def color_picker_popup(self, tag: str): + bs.app.classic.accounts.have_pro = lambda: True + CustomColorPicker(parent=self._root_widget, + position=(0, 0), + initial_color=config[tag], + delegate=self, + tag=tag) + + def color_picker_selected_color(self, picker: CustomColorPicker, color: Sequence[float, float, float]): + if not self._root_widget: + return + self.update_color(tag=picker.get_tag(), color=color) + self.update_buttons() + + def color_picker_closing(self, picker: ColorPicker): + bs.app.classic.accounts.have_pro = original_unlocked_pro + + def free_edit_enter(self, window: FreeEditWindow, c: Any, txt: Any): + bs.app.config[c] = float(txt) + bs.app.config.apply_and_commit() + self.update_buttons() + + def update_buttons(self): + # menu labels + bui.buttonwidget(edit=self._menu_map_button, label=config['Menu Map']) + bui.buttonwidget(edit=self._menu_music_button, label=config['Menu Music']) + bui.buttonwidget(edit=self._menu_reflectiont_button, label=config['Menu Reflection Type']) + + # menu colors + bui.buttonwidget(edit=self._menu_tint_button, color=config['Menu Tint']) + bui.buttonwidget(edit=self._menu_ambient_button, color=config['Menu Ambient']) + bui.buttonwidget(edit=self._menu_vignetteO_button, color=config['Menu Vignette Outer']) + bui.buttonwidget(edit=self._menu_vignetteI_button, color=config['Menu Vignette Inner']) + bui.buttonwidget(edit=self._menu_rcolor_button, color=config['Menu Map Color']) + + # menu values + bui.buttonwidget(edit=self._menu_reflections_button, + label=str(config['Menu Reflection Scale'])) + bui.buttonwidget(edit=self._menu_cameramode_button, label=str(config['Menu Camera Mode'])) + bui.checkboxwidget(edit=self._menu_logotext_button.widget, value=config['Menu Logo Text']) + + def update_color(self, tag: str, color: tuple[float, float, float]): + bs.app.config[tag] = color + bs.app.config.apply_and_commit() + + def reset_config(self): + handle_config([ + "The Pad (with trees)", + (1.14, 1.1, 1.0), (1.06, 1.04, 1.03), + (0.45, 0.55, 0.54), (0.99, 0.98, 0.98), "Menu", + (1.0, 1.0, 1.0), 0.3, 'None', 'rotate', + True, True, True, True, True, True, True, True, True + ], False + ) + self.update_buttons() + bui.screenmessage('Reset Settings', color=(0, 1, 0)) + + def close(self): + self._accounts_window = None + bui.containerwidget(edit=self._root_widget, transition='out_scale') + + def close_all(self): + accounts_window = self._accounts_window + bui.containerwidget(edit=self._root_widget, transition='out_scale') + accounts_window._back(False) + + +class MainMenuTheme(MainMenuActivity): + + def _start_preloads(self): + if self.expired: + return + with self.context: + _preload1() + + bui.apptimer(0.5, self._start_menu_music) + + def _start_menu_music(self): + music = GLOBALS_MUSIC.get(config['Menu Music']) + if music is not None: + bs.setmusic(music) + + def _make_word(self, *args, **kwargs) -> None: + if not config['Menu Logo Text']: + return + super()._make_word(*args, **kwargs) + + def _make_logo(self, *args, **kwargs) -> None: + if not config['Menu Logo Text']: + return + super()._make_logo(*args, **kwargs) + + def on_transition_in(self): + bs.Activity.on_transition_in(self) + random.seed(123) + app = bs.app + env = ba.app.env + assert app.classic is not None + + plus = bui.app.plus + assert plus is not None + + vr_mode = env.vr + + if not toolbar_test: + color = (1.0, 1.0, 1.0, 1.0) if vr_mode else (0.5, 0.6, 0.5, 0.6) + + scale = ( + 0.9 + if (app.ui_v1.uiscale is bs.UIScale.SMALL or vr_mode) + else 0.7 + ) + self.my_name = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'v_attach': 'bottom', + 'h_align': 'center', + 'color': color, + 'flatness': 1.0, + 'shadow': 1.0 if vr_mode else 0.5, + 'scale': scale, + 'position': (0, 10), + 'vr_depth': -10, + 'text': '\xa9 2011-2023 Eric Froemling', + }, + ) + ) + + tval = bs.Lstr( + resource='hostIsNavigatingMenusText', + subs=[('${HOST}', plus.get_v1_account_display_string())], + ) + self._host_is_navigating_text = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'text': tval, + 'client_only': True, + 'position': (0, -200), + 'flatness': 1.0, + 'h_align': 'center', + }, + ) + ) + if not app.classic.main_menu_did_initial_transition and hasattr( + self, 'my_name' + ): + assert self.my_name is not None + assert self.my_name.node + bs.animate(self.my_name.node, 'opacity', {2.3: 0, 3.0: 1.0}) + + vr_mode = env.vr + uiscale = bui.UIV1Subsystem.uiscale + + force_show_build_number = False + + if not toolbar_test: + if env.debug or env.test or force_show_build_number: + if env.debug: + text = bs.Lstr( + value='${V} (${B}) (${D})', + subs=[ + ('${V}', env.version), + ('${B}', str(env.build_number)), + ('${D}', bs.Lstr(resource='debugText')), + ], + ) + else: + text = bs.Lstr( + value='${V} (${B})', + subs=[ + ('${V}', env.version), + ('${B}', str(env.build_number)), + ], + ) + else: + text = bs.Lstr(value='${V}', subs=[('${V}', env.version)]) + scale = 0.9 if (uiscale is bs.UIScale.SMALL or vr_mode) else 0.7 + color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7) + self.version = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'v_attach': 'bottom', + 'h_attach': 'right', + 'h_align': 'right', + 'flatness': 1.0, + 'vr_depth': -10, + 'shadow': 1.0 if vr_mode else 0.5, + 'color': color, + 'scale': scale, + 'position': (-260, 10) if vr_mode else (-10, 10), + 'text': text, + }, + ) + ) + if not app.classic.main_menu_did_initial_transition: + assert self.version.node + bs.animate(self.version.node, 'opacity', {2.3: 0, 3.0: 1.0}) + + self.beta_info = self.beta_info_2 = None + if env.test and not (env.demo or env.arcade) and config['Menu Logo Text']: + pos = (230, 35) + self.beta_info = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'v_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 1, 1), + 'shadow': 0.5, + 'flatness': 0.5, + 'scale': 1, + 'vr_depth': -60, + 'position': pos, + 'text': bs.Lstr(resource='testBuildText'), + }, + ) + ) + if not app.classic.main_menu_did_initial_transition: + assert self.beta_info.node + bs.animate(self.beta_info.node, 'opacity', {1.3: 0, 1.8: 1.0}) + + b = GLOBALS_MAPDATA.get(config["Menu Map"]).get("Camera Bounds") + b = ( + b[0] - b[3] / 2.0, + b[1] - b[4] / 2.0, + b[2] - b[5] / 2.0, + b[0] + b[3] / 2.0, + b[1] + b[4] / 2.0, + b[2] + b[5] / 2.0 + ) + + gnode = self.globalsnode + gnode.camera_mode = 'follow' if config["Menu Camera Mode"] == 'static' else 'rotate' + + self.load_map_stuff() + gnode.tint = config["Menu Tint"] + gnode.ambient_color = config["Menu Ambient"] + gnode.vignette_outer = config["Menu Vignette Outer"] + gnode.vignette_inner = config["Menu Vignette Inner"] + gnode.area_of_interest_bounds = b + + self.main.node.color = config["Menu Map Color"] + self.main.node.reflection = GLOBALS_REFLECTION[config["Menu Reflection Type"]] + self.main.node.reflection_scale = [float(config["Menu Reflection Scale"])] + + self._update_timer = bs.Timer(1.0, self._update, repeat=True) + self._update() + + bui.add_clean_frame_callback(bs.WeakCall(self._start_preloads)) + + random.seed() + + if not (env.demo or env.arcade) and not toolbar_test: + self._news = NewsDisplay(self) + + with bs.ContextRef.empty(): + from bauiv1lib import specialoffer + + assert bs.app.classic is not None + if bool(False): + uicontroller = bs.app.ui_v1.controller + assert uicontroller is not None + uicontroller.show_main_menu() + else: + main_menu_location = bs.app.ui_v1.get_main_menu_location() + + # When coming back from a kiosk-mode game, jump to + # the kiosk start screen. + if env.demo or env.arcade: + # pylint: disable=cyclic-import + from bauiv1lib.kiosk import KioskWindow + + if TARGET_BALLISTICA_BUILD < 21697: + bs.app.ui_v1.set_main_menu_window( + KioskWindow().get_root_widget(), + ) + else: + bs.app.ui_v1.set_main_menu_window( + KioskWindow().get_root_widget(), from_window=False + ) + # ..or in normal cases go back to the main menu + else: + if main_menu_location == 'Gather': + # pylint: disable=cyclic-import + from bauiv1lib.gather import GatherWindow + + if TARGET_BALLISTICA_BUILD < 21697: + bs.app.ui_v1.set_main_menu_window( + GatherWindow(transition=None).get_root_widget(), + ) + else: + bs.app.ui_v1.set_main_menu_window( + GatherWindow(transition=None).get_root_widget(), from_window=False + ) + elif main_menu_location == 'Watch': + # pylint: disable=cyclic-import + from bauiv1lib.watch import WatchWindow + + if TARGET_BALLISTICA_BUILD < 21697: + bs.app.ui_v1.set_main_menu_window( + WatchWindow(transition=None).get_root_widget(), + ) + else: + bs.app.ui_v1.set_main_menu_window( + WatchWindow(transition=None).get_root_widget(), from_window=False + ) + elif main_menu_location == 'Team Game Select': + # pylint: disable=cyclic-import + from bauiv1lib.playlist.browser import ( + PlaylistBrowserWindow, + ) + + if TARGET_BALLISTICA_BUILD < 21697: + bs.app.ui_v1.set_main_menu_window( + PlaylistBrowserWindow( + sessiontype=bs.DualTeamSession, transition=None + ).get_root_widget(), + ) + else: + bs.app.ui_v1.set_main_menu_window( + PlaylistBrowserWindow( + sessiontype=bs.DualTeamSession, transition=None + ).get_root_widget(), from_window=False + ) + elif main_menu_location == 'Free-for-All Game Select': + # pylint: disable=cyclic-import + from bauiv1lib.playlist.browser import ( + PlaylistBrowserWindow, + ) + + if TARGET_BALLISTICA_BUILD < 21697: + bs.app.ui_v1.set_main_menu_window( + PlaylistBrowserWindow( + sessiontype=bs.FreeForAllSession, + transition=None, + ).get_root_widget(), + ) + else: + bs.app.ui_v1.set_main_menu_window( + PlaylistBrowserWindow( + sessiontype=bs.FreeForAllSession, + transition=None, + ).get_root_widget(), from_window=False + ) + elif main_menu_location == 'Coop Select': + # pylint: disable=cyclic-import + from bauiv1lib.coop.browser import CoopBrowserWindow + + if TARGET_BALLISTICA_BUILD < 21697: + bs.app.ui_v1.set_main_menu_window( + CoopBrowserWindow(transition=None).get_root_widget(), + ) + else: + bs.app.ui_v1.set_main_menu_window( + CoopBrowserWindow(transition=None).get_root_widget(), from_window=False + ) + elif main_menu_location == 'Benchmarks & Stress Tests': + # pylint: disable=cyclic-import + from bauiv1lib.debug import DebugWindow + + if TARGET_BALLISTICA_BUILD < 21697: + bs.app.ui_v1.set_main_menu_window( + DebugWindow(transition=None).get_root_widget(), + ) + else: + bs.app.ui_v1.set_main_menu_window( + DebugWindow(transition=None).get_root_widget(), from_window=False + ) + else: + # pylint: disable=cyclic-import + from bauiv1lib.mainmenu import MainMenuWindow + + if TARGET_BALLISTICA_BUILD < 21697: + bs.app.ui_v1.set_main_menu_window( + MainMenuWindow(transition=None).get_root_widget(), + ) + else: + bs.app.ui_v1.set_main_menu_window( + MainMenuWindow(transition=None).get_root_widget(), from_window=False + ) + + if not specialoffer.show_offer(): + + def try_again(): + if not specialoffer.show_offer(): + bui.apptimer(2.0, specialoffer.show_offer) + + bui.apptimer(2.0, try_again) + app.classic.main_menu_did_initial_transition = True + + def load_map_stuff(self): + m = bs.getmesh + t = bs.gettexture + map_type = config["Menu Map"] + if map_type == "The Pad (with trees)": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('thePadLevel'), + 'color_texture': t('thePadLevelColor'), + 'reflection': 'soft', + 'reflection_scale': [0.3] + })) + self.trees = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('trees'), + 'lighting': False, + 'reflection': 'char', + 'reflection_scale': [0.1], + 'color_texture': t('treesColor') + })) + self.bgterrain = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('thePadBG'), + 'color': (0.92, 0.91, 0.9), + 'lighting': False, + 'background': True, + 'color_texture': t('menuBG'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('thePadLevelBottom'), + 'lighting': False, + 'reflection': 'soft', + 'reflection_scale': [0.45], + 'color_texture': t('thePadLevelColor') + })) + elif map_type == "The Pad": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('thePadLevel'), + 'color_texture': t('thePadLevelColor'), + 'reflection': 'soft', + 'reflection_scale': [0.3] + })) + self.bgterrain = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('thePadBG'), + 'color': (0.92, 0.91, 0.9), + 'lighting': False, + 'background': True, + 'color_texture': t("menuBG") + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('thePadLevelBottom'), + 'lighting': False, + 'reflection': 'soft', + 'reflection_scale': [0.45], + 'color_texture': t('thePadLevelColor') + })) + elif map_type == "Hockey Stadium": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('hockeyStadiumOuter'), + 'color_texture': t('hockeyStadium'), + 'reflection': 'soft', + 'reflection_scale': [0.3] + })) + self.inner = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('hockeyStadiumInner'), + 'opacity': 0.92, + 'opacity_in_low_or_ui_medium_quality': 1.0, + 'color_texture': t('hockeyStadium') + })) + self.stands = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('hockeyStadiumStands'), + 'visible_in_reflections': False, + 'color_texture': t('footballStadium') + })) + elif map_type == "Football Stadium": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('footballStadium'), + 'color_texture': t('footballStadium'), + })) + elif map_type == "Bridgit": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('bridgitLevelTop'), + 'color_texture': t('bridgitLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('bridgitLevelBottom'), + 'lighting': False, + 'color_texture': t('bridgitLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('natureBackground'), + 'lighting': False, + 'background': True, + 'color_texture': t('natureBackgroundColor'), + })) + elif map_type == "Big G": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('bigG'), + 'color': (0.7, 0.7, 0.7), + 'color_texture': t('bigG'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('bigGBottom'), + 'lighting': False, + 'color': (0.7, 0.7, 0.7), + 'color_texture': t('bigG'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('natureBackground'), + 'lighting': False, + 'background': True, + 'color_texture': t('natureBackgroundColor'), + })) + elif map_type == "Roundabout": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('roundaboutLevel'), + 'color': (0.7, 0.7, 0.7), + 'color_texture': t('roundaboutLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('roundaboutLevelBottom'), + 'lighting': False, + 'color': (0.7, 0.7, 0.7), + 'color_texture': t('roundaboutLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('natureBackground'), + 'lighting': False, + 'background': True, + 'color_texture': t('natureBackgroundColor'), + })) + elif map_type == "Monkey Face": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('monkeyFaceLevel'), + 'color_texture': t('monkeyFaceLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('monkeyFaceLevelBottom'), + 'lighting': False, + 'color_texture': t('monkeyFaceLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('natureBackground'), + 'lighting': False, + 'color_texture': t('natureBackgroundColor'), + })) + elif map_type == "Monkey Face": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('monkeyFaceLevel'), + 'color_texture': t('monkeyFaceLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('monkeyFaceLevelBottom'), + 'lighting': False, + 'color_texture': t('monkeyFaceLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('natureBackground'), + 'lighting': False, + 'color_texture': t('natureBackgroundColor'), + })) + elif map_type == "Zig Zag": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('zigZagLevel'), + 'color_texture': t('zigZagLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('zigZagLevelBottom'), + 'lighting': False, + 'color_texture': t('zigZagLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('natureBackground'), + 'lighting': False, + 'color_texture': t('natureBackgroundColor'), + })) + elif map_type == "Doom Shroom": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('doomShroomLevel'), + 'color_texture': t('doomShroomLevelColor'), + })) + self.stem = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('doomShroomStem'), + 'lighting': False, + 'color_texture': t('doomShroomLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('doomShroomBG'), + 'lighting': False, + 'background': True, + 'color_texture': t('doomShroomBGColor'), + })) + elif map_type == "Lake Frigid": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('lakeFrigid'), + 'color_texture': t('lakeFrigid'), + })) + self.top = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('lakeFrigidTop'), + 'lighting': False, + 'color_texture': t('lakeFrigid'), + })) + self.reflections = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('lakeFrigidReflections'), + 'lighting': False, + 'overlay': True, + 'opacity': 0.15, + 'color_texture': t('lakeFrigidReflections'), + })) + elif map_type == "Tip Top": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('tipTopLevel'), + 'color': (0.7, 0.7, 0.7), + 'color_texture': t('tipTopLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('tipTopLevelBottom'), + 'lighting': False, + 'color': (0.7, 0.7, 0.7), + 'color_texture': t('tipTopLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('tipTopBG'), + 'lighting': False, + 'color': (0.4, 0.4, 0.4), + 'background': True, + 'color_texture': t('tipTopBGColor'), + })) + elif map_type == "Crag Castle": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('cragCastleLevel'), + 'color_texture': t('cragCastleLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('cragCastleLevelBottom'), + 'lighting': False, + 'color_texture': t('cragCastleLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('thePadBG'), + 'lighting': False, + 'background': True, + 'color_texture': t('menuBG'), + })) + elif map_type == "Tower D": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('towerDLevel'), + 'color_texture': t('towerDLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('towerDLevelBottom'), + 'lighting': False, + 'color_texture': t('towerDLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('thePadBG'), + 'lighting': False, + 'background': True, + 'color_texture': t('menuBG'), + })) + elif map_type == "Happy Thoughts": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('alwaysLandLevel'), + 'color_texture': t('alwaysLandLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('alwaysLandLevelBottom'), + 'lighting': False, + 'color_texture': t('alwaysLandLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('alwaysLandBG'), + 'lighting': False, + 'background': True, + 'color_texture': t('alwaysLandBGColor'), + })) + elif map_type == "Step Right Up": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('stepRightUpLevel'), + 'color_texture': t('stepRightUpLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('stepRightUpLevelBottom'), + 'lighting': False, + 'color_texture': t('stepRightUpLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('thePadBG'), + 'lighting': False, + 'background': True, + 'color_texture': t('menuBG'), + })) + elif map_type == "Courtyard": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('courtyardLevel'), + 'color_texture': t('courtyardLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('courtyardLevelBottom'), + 'lighting': False, + 'color_texture': t('courtyardLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('thePadBG'), + 'lighting': False, + 'background': True, + 'color_texture': t('menuBG'), + })) + elif map_type == "Rampage": + self.main = bs.NodeActor(bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'mesh': m('rampageLevel'), + 'color_texture': t('rampageLevelColor'), + })) + self.bottom = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('rampageLevelBottom'), + 'lighting': False, + 'color_texture': t('rampageLevelColor'), + })) + self.background = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('rampageBG'), + 'lighting': False, + 'background': True, + 'color_texture': t('rampageBGColor'), + })) + self.background_2 = bs.NodeActor(bs.newnode( + 'terrain', + attrs={ + 'mesh': m('rampageBG2'), + 'lighting': False, + 'background': True, + 'color_texture': t('rampageBGColor2'), + })) + + +def menu_theme(self): + this_class = self + MenuThemeWindow(accounts_window=this_class) + + +def handle_config(keys: List[str], adding: bool = False): + our_config = { + "Menu Map": keys[0], + "Menu Tint": keys[1], + "Menu Ambient": keys[2], + "Menu Vignette Outer": keys[3], + "Menu Vignette Inner": keys[4], + "Menu Music": keys[5], + "Menu Map Color": keys[6], + "Menu Reflection Scale": keys[7], + "Menu Reflection Type": keys[8], + "Menu Camera Mode": keys[9], + "Menu Logo Text": keys[10], + "vignette_outer": keys[11], + "vignette_inner": keys[12], + "ambient_color": keys[13], + "tint": keys[14], + "map_color": keys[15], + "map_reflection_scale": keys[16], + "map_reflection_type": keys[17] + } + config_keys = list(our_config.keys()) + p = 0 + + for cf in config_keys: + if cf not in bs.app.config and adding: + config[cf] = keys[p] + elif our_config[cf] is not None and not adding: + config[cf] = keys[p] + p += 1 + bs.app.config.apply_and_commit() + + +def new_init(self, *args, **kwargs): + original_account_init(self, *args, **kwargs) + + self._menu_theme = bui.buttonwidget( + parent=self._root_widget, + position=((470, 330) if ui_type is ui_small else + (420, 434) if ui_type is ui_large else (445, 374)), + scale=(1.2 if ui_type is ui_small else + 1.3 if ui_type is ui_large else 1.0), + size=(160, 30) if ui_type is ui_small else (167, 30), + color=(0.55, 0.7, 0.63), + text_scale=(0.7 if ui_type is ui_medium else + 0.65 if ui_type is ui_large else 0.5), + autoselect=False, + button_type="regular", + label="Menu Theme", + on_activate_call=self.menu_theme + ) + self.previous_config = { + "Menu Map": config["Menu Map"], + "Menu Tint": config["Menu Tint"], + "Menu Ambient": config["Menu Ambient"], + "Menu Vignette Outer": config["Menu Vignette Outer"], + "Menu Vignette Inner": config["Menu Vignette Inner"], + "Menu Music": config["Menu Music"], + "Menu Map Color": config["Menu Map Color"], + "Menu Reflection Scale": config["Menu Reflection Scale"], + "Menu Reflection Type": config["Menu Reflection Type"], + "Menu Camera Mode": config["Menu Camera Mode"], + "Menu Logo Text": config["Menu Logo Text"] + } + + +def new_back(self, save_state: bool = True): + assert bui.app.classic is not None + if save_state: + self._save_state() + + bui.containerwidget(edit=self._root_widget, transition=self._transition_out) + + main_menu_window = MainMenuWindow(transition='in_left').get_root_widget() + if TARGET_BALLISTICA_BUILD < 21697: + bui.app.ui_v1.set_main_menu_window(main_menu_window,) + else: + bui.app.ui_v1.set_main_menu_window(main_menu_window, from_window=False) + + current_config = { + "Menu Map": config["Menu Map"], + "Menu Tint": config["Menu Tint"], + "Menu Ambient": config["Menu Ambient"], + "Menu Vignette Outer": config["Menu Vignette Outer"], + "Menu Vignette Inner": config["Menu Vignette Inner"], + "Menu Music": config["Menu Music"], + "Menu Map Color": config["Menu Map Color"], + "Menu Reflection Scale": config["Menu Reflection Scale"], + "Menu Reflection Type": config["Menu Reflection Type"], + "Menu Camera Mode": config["Menu Camera Mode"], + "Menu Logo Text": config["Menu Logo Text"] + } + + for x in self.previous_config: + if current_config[x] != self.previous_config[x]: + bs.pushcall(lambda: bs.new_host_session(menu.MainMenuSession)) + break + + +# ba_meta export plugin +class Plugin(ba.Plugin): + def on_app_running(self): + AccountSettingsWindow.__init__ = new_init + AccountSettingsWindow._back = new_back + AccountSettingsWindow.menu_theme = menu_theme + + menu.MainMenuActivity = MainMenuTheme + + handle_config([ + "The Pad (with trees)", + (1.14, 1.1, 1.0), (1.06, 1.04, 1.03), + (0.45, 0.55, 0.54), (0.99, 0.98, 0.98), "Menu", + (1.0, 1.0, 1.0), 0.3, 'None', 'rotate', + True, True, True, True, True, True, True, True, True + ], True + ) diff --git a/plugins/utilities/mood_light.py b/plugins/utilities/mood_light.py new file mode 100644 index 000000000..f60e6fe96 --- /dev/null +++ b/plugins/utilities/mood_light.py @@ -0,0 +1,338 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 9 +from __future__ import annotations +from typing import TYPE_CHECKING, cast +if TYPE_CHECKING: + from typing import Any, Sequence, Callable, List, Dict, Tuple, Optional, Union + +import random +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +from bascenev1._map import Map +from bascenev1lib import mainmenu +from bauiv1lib.mainmenu import MainMenuWindow +from bauiv1lib.party import PartyWindow +from bascenev1lib.gameutils import SharedObjects + +"""mood light plugin by ʟօʊքɢǟʀօʊ +type ml in chat or use plugin manager to open settings""" + + +def Print(*args): + out = "" + for arg in args: + a = str(arg) + out += a + bui.screenmessage(out) + + +def cprint(*args): + out = "" + for arg in args: + a = str(arg) + out += a + bs.chatmessage(out) + + +try: + Ldefault, Udefault = babase.app.config.get("moodlightingSettings") +except: + babase.app.config["moodlightingSettings"] = (15, 20) + Ldefault, Udefault = babase.app.config.get("moodlightingSettings") + Print("settings up moodlight") + Print("Type ml in chat or use plugin manager to access settings") + +try: + loop = babase.app.config.get("moodlightEnabled") +except: + babase.app.config["moodlightEnabled"] = True + babase.app.config.commit() + loop = True + + +class SettingWindow(bui.Window): + def __init__(self): + self.draw_ui() + + def increase_limit(self): + global Ldefault, Udefault + try: + if Udefault >= 29 and self.selected == "upper": + bui.textwidget(edit=self.warn_text, + text="Careful!You risk get blind beyond this point") + elif self.selected == "lower" and Ldefault >= -20 or self.selected == "upper" and Udefault <= 30: + bui.textwidget(edit=self.warn_text, text="") + if self.selected == "lower": + Ldefault += 1 + bui.textwidget(edit=self.lower_text, text=str(Ldefault)) + elif self.selected == "upper": + Udefault += 1 + bui.textwidget(edit=self.upper_text, text=str(Udefault)) + except AttributeError: + bui.textwidget(edit=self.warn_text, text="Click on number to select it") + + def decrease_limit(self): + global Ldefault, Udefault + try: + if Ldefault <= -19 and self.selected == "lower": + bui.textwidget(edit=self.warn_text, + text="DON'T BE AFRAID OF DARK,IT'S A PLACE WHERE YOU CAN HIDE") + elif (self.selected == "upper" and Udefault <= 30) or (self.selected == "lower" and Ldefault >= -20): + bui.textwidget(edit=self.warn_text, text="") + if self.selected == "lower": + Ldefault -= 1 + bui.textwidget(edit=self.lower_text, text=str(Ldefault)) + elif self.selected == "upper": + Udefault -= 1 + bui.textwidget(edit=self.upper_text, text=str(Udefault)) + except AttributeError: + bui.textwidget(edit=self.warn_text, text="Click on number to select it") + + def on_text_click(self, selected): + self.selected = selected + if selected == "upper": + bui.textwidget(edit=self.upper_text, color=(0, 0, 1)) + bui.textwidget(edit=self.lower_text, color=(1, 1, 1)) + elif selected == "lower": + bui.textwidget(edit=self.lower_text, color=(0, 0, 1)) + bui.textwidget(edit=self.upper_text, color=(1, 1, 1)) + else: + Print("this should't happen from on_text_click") + + def draw_ui(self): + self.uiscale = bui.app.ui_v1.uiscale + + super().__init__( + root_widget=bui.containerwidget( + size=(670, 670), + on_outside_click_call=self.close, + transition="in_right",)) + + moodlight_label = bui.textwidget( + parent=self._root_widget, + size=(200, 100), + position=(150, 550), + scale=2, + selectable=False, + h_align="center", + v_align="center", + text="Mood light settings", + color=(0, 1, 0)) + + self.enable_button = bui.buttonwidget( + parent=self._root_widget, + position=(100, 470), + size=(90, 70), + scale=1.5, + color=(1, 0, 0) if loop else (0, 1, 0), + label="DISABLE" if loop else "ENABLE", + on_activate_call=self.on_enableButton_press) + + save_button = bui.buttonwidget( + parent=self._root_widget, + position=(520, 470), + size=(90, 70), + scale=1.5, + label="SAVE", + on_activate_call=self.save_settings) + + self.close_button = bui.buttonwidget( + parent=self._root_widget, + position=(550, 590), + size=(35, 35), + icon=bui.gettexture("crossOut"), + icon_color=(1, 0.2, 0.2), + scale=2, + color=(1, 0.2, 0.2), + extra_touch_border_scale=5, + on_activate_call=self.close) + + self.lower_text = bui.textwidget( + parent=self._root_widget, + size=(200, 100), + scale=2, + position=(100, 200), + h_align="center", + v_align="center", + maxwidth=400.0, + text=str(Ldefault), + click_activate=True, + selectable=True) + + lower_text_label = bui.textwidget( + parent=self._root_widget, + size=(200, 100), + position=(100, 150), + h_align="center", + v_align="center", + text="Limit darkness") + + self.upper_text = bui.textwidget( + parent=self._root_widget, + size=(200, 100), + scale=2, + position=(400, 200), + h_align="center", + v_align="center", + maxwidth=400.0, + text=str(Udefault), + click_activate=True, + selectable=True) + + upper_text_label = bui.textwidget( + parent=self._root_widget, + size=(200, 100), + position=(400, 150), + h_align="center", + v_align="center", + text="Limit brightness") + + decrease_button = bui.buttonwidget( + parent=self._root_widget, + position=(100, 100), + size=(5, 1), + scale=3.5, + extra_touch_border_scale=2.5, + icon=bui.gettexture("downButton"), + on_activate_call=self.decrease_limit) + + increase_button = bui.buttonwidget( + parent=self._root_widget, + position=(600, 100), + size=(5, 1), + scale=3.5, + extra_touch_border_scale=2.5, + icon=bui.gettexture("upButton"), + on_activate_call=self.increase_limit) + + self.warn_text = bui.textwidget( + parent=self._root_widget, + text="", + size=(400, 200), + position=(150, 300), + h_align="center", + v_align="center", + maxwidth=600) + +# ++++++++++++++++for keyboard navigation++++++++++++++++ + bui.widget(edit=self.enable_button, up_widget=decrease_button, + down_widget=self.lower_text, left_widget=save_button, right_widget=save_button) + bui.widget(edit=save_button, up_widget=self.close_button, down_widget=self.upper_text, + left_widget=self.enable_button, right_widget=self.enable_button) + bui.widget(edit=self.close_button, up_widget=increase_button, down_widget=save_button, + left_widget=self.enable_button, right_widget=save_button) + bui.widget(edit=self.lower_text, up_widget=self.enable_button, down_widget=decrease_button, + left_widget=self.upper_text, right_widget=self.upper_text) + bui.widget(edit=self.upper_text, up_widget=save_button, down_widget=increase_button, + left_widget=self.lower_text, right_widget=self.lower_text) + bui.widget(edit=decrease_button, up_widget=self.lower_text, down_widget=self.enable_button, + left_widget=increase_button, right_widget=increase_button) + bui.widget(edit=increase_button, up_widget=self.upper_text, down_widget=self.close_button, + left_widget=decrease_button, right_widget=decrease_button) +# -------------------------------------------------------------------------------------------------- + + bui.textwidget(edit=self.upper_text, on_activate_call=babase.Call( + self.on_text_click, "upper")) + bui.textwidget(edit=self.lower_text, on_activate_call=babase.Call( + self.on_text_click, "lower")) + + def on_enableButton_press(self): + global loop + loop = babase.app.config.get("moodlightEnabled") + if loop: + loop = False + label = "ENABLE" + color = (0, 1, 0) + elif not loop: + loop = True + label = "DISABLE" + color = (1, 0, 0) + in_game = not isinstance(bs.get_foreground_host_session(), mainmenu.MainMenuSession) + if in_game: + Print("Restart level to apply") + babase.app.config["moodlightEnabled"] = loop + babase.app.config.commit() + bui.buttonwidget(edit=self.enable_button, label=label, color=color) + + def save_settings(self): + babase.app.config["moodlightingSettings"] = (Ldefault, Udefault) + babase.app.config.commit() + Print("settings saved") + self.close() + + def close(self): + bui.containerwidget(edit=self._root_widget, transition="out_right",) + + +def new_chat_message(msg: Union[str, babase.Lstr], clients: Sequence[int] = None, sender_override: str = None): + old_fcm(msg, clients, sender_override) + if msg == 'ml': + try: + global Ldefault, Udefault + Ldefault, Udefault = babase.app.config.get("moodlightingSettings") + SettingWindow() + cprint("Mood light settings opened") + except Exception as err: + Print(err, "-from new_chat_message") + + +class NewMainMenuWindow(MainMenuWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Display chat icon, but if user open/close gather it may disappear + bui.set_party_icon_always_visible(True) + + +old_fcm = bs.chatmessage +bs.chatmessage = new_chat_message +Map._old_init = Map.__init__ + +# ba_meta export babase.Plugin + + +class moodlight(babase.Plugin): + def __init__(self): + pass + + def on_app_running(self): + _babase.show_progress_bar() + MainMenuWindow = NewMainMenuWindow + + def show_settings_ui(self, source_widget): + SettingWindow() + + def has_settings_ui(self): + return True + + def show_settings_ui(self, button): + SettingWindow() + + def _new_init(self, vr_overlay_offset: Optional[Sequence[float]] = None) -> None: + self._old_init(vr_overlay_offset) + in_game = not isinstance(bs.get_foreground_host_session(), mainmenu.MainMenuSession) + if not in_game: + return + + gnode = bs.getactivity().globalsnode + default_tint = (1.100000023841858, 1.0, 0.8999999761581421) + transition_duration = 1.0 # for future improvements + + def changetint(): + if loop: + Range = (random.randrange(Ldefault, Udefault)/10, random.randrange(Ldefault, + Udefault)/10, random.randrange(Ldefault, Udefault)/10) + bs.animate_array(gnode, 'tint', 3, { + 0.0: gnode.tint, + transition_duration: Range + }) + else: + global timer + timer = None + bs.animate_array(gnode, "tint", 3, {0.0: gnode.tint, 0.4: default_tint}) + + global timer + timer = bs.Timer(0.3, changetint, repeat=True) + + Map.__init__ = _new_init diff --git a/plugins/utilities/natpmp_upnp.py b/plugins/utilities/natpmp_upnp.py new file mode 100644 index 000000000..5e578a916 --- /dev/null +++ b/plugins/utilities/natpmp_upnp.py @@ -0,0 +1,432 @@ +# ba_meta require api 9 +# crafted by brostos +#! Try patching upnpclient to use defusedxml replacement for lxml for more device support +import babase +import bauiv1 as bui +import bascenev1 as bs + +import shutil +import platform +import os +import hashlib +import zipfile +import tarfile +import threading +import ast +import time +from urllib.parse import urlparse, unquote +from pathlib import Path +from os import remove, getcwd +from urllib.request import urlretrieve, urlopen + + +# Plucked from https://github.com/ethereum/upnp-port-forward/blob/master/upnp_port_forward/ +WAN_SERVICE_NAMES = ( + "WANIPConn1", + "WANIPConnection.1", # Nighthawk C7800 + "WANPPPConnection.1", # CenturyLink C1100Z + "WANPPPConn1", # Huawei B528s-23a +) +BS_PORT = bs.get_game_port() + + +def threaded(func): + def wrapper(*args, **kwargs): + thread = threading.Thread( + target=func, args=args, kwargs=kwargs, name=func.__name__ + ) + thread.start() + + return wrapper + + +@threaded +def get_modules() -> None: + if babase.app.classic.platform == "mac": + install_path = bs.app.env.python_directory_app + else: + install_path = Path( + f"{getcwd()}/ba_data/python" + ) # For the guys like me on windows + packages = { + "upnp-client": { + "url": "https://files.pythonhosted.org/packages/dd/69/4d38d9fa757c328df93e7037eb8c1da8ca48e62828c23ba3c421e9335e30/upnpclient-1.0.3.tar.gz", + "md5": "f936c8de89705555f6bd736a66d3af5d", + "folder": "upnpclient", + }, + "python-dateutil": { + "url": "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", + "md5": "81cb6aad924ef40ebfd3d62eaebe47c6", + "folder": "dateutil", + }, + "six": { + "url": "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", + "md5": "a0387fe15662c71057b4fb2b7aa9056a", + "folder": "six.py", + }, + "requests": { + "url": "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", + "md5": "fa3ee5ac3f1b3f4368bd74ab530d3f0f", + "folder": "requests", + }, + "idna": { + "url": "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", + "md5": "28448b00665099117b6daa9887812cc4", + "folder": "idna", + }, + #! Api 9 already has urllib3 module + # "urllib3": { + # "url": "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz", + # "md5": "872f7f43af1b48e7c116c7542ab39fab", + # "folder": "urllib3", + # }, + "ifaddr": { + "url": "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", + "md5": "b1cac02b5dc354d68dd6d853bc9565a7", + "folder": "ifaddr", + }, + "NAT-PMP": { + "url": "https://files.pythonhosted.org/packages/dc/0c/28263fb4a623e6718a179bca1f360a6ae38f0f716a6cacdf47e15a5fa23e/NAT-PMP-1.3.2.tar.gz", + "md5": "7e5faa22acb0935f75664e9c4941fda4", + "folder": "natpmp", + }, + } + + system = platform.platform() + + if "Windows" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/36/88/684d4e800f5aa28df2a991a6a622783fb73cf0e46235cfa690f9776f032e/lxml-5.3.0-cp312-cp312-win32.whl", + "md5": "a5579cb068a3fbfb5989fbeb4024c599", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", + "md5": "babec153025b1270d6a2fd76e2c3772f", + "folder": "charset_normalizer", + } + elif "Darwin" in system and "arm64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/eb/6d/d1f1c5e40c64bf62afd7a3f9b34ce18a586a1cccbf71e783cd0a6d8e8971/lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", + "md5": "0200ca09c13892c80b47cf4c713786ed", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", + "md5": "572c9f4f64469518d6a6b4c15710201a", + "folder": "charset_normalizer", + } + elif "Darwin" in system and "x86_64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/bd/83/26b1864921869784355459f374896dcf8b44d4af3b15d7697e9156cb2de9/lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", + "md5": "96b82c1e6d24472af28c48d9bb21605e", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", + "md5": "572c9f4f64469518d6a6b4c15710201a", + "folder": "charset_normalizer", + } + elif "glibc" in system and "x86_64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/0a/6e/94537acfb5b8f18235d13186d247bca478fea5e87d224644e0fe907df976/lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", + "md5": "d63bf3d33e46a3b0262176b1a815b4b0", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "md5": "1edb315f82fa657b8ee5d564117e057c", + "folder": "charset_normalizer", + } + elif "glibc" in system and "aarch64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/88/69/6972bfafa8cd3ddc8562b126dd607011e218e17be313a8b1b9cc5a0ee876/lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "md5": "663ccdccd076b26b5607901799c671be", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "md5": "d2e8c76665fb9fb013882d4052f46b95", + "folder": "charset_normalizer", + } + elif not "glibc" in system and "x86_64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/7d/ed/e6276c8d9668028213df01f598f385b05b55a4e1b4662ee12ef05dab35aa/lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", + "md5": "659bdaee4672e8409b277b570e3e3e39", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", + "md5": "7a60860d64616d5a0af22d034963ab11", + "folder": "charset_normalizer", + } + elif not "glibc" in system and "aarch64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/8d/e8/4b15df533fe8e8d53363b23a41df9be907330e1fa28c7ca36893fad338ee/lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", + "md5": "3ec71cd198cc28525f4c1d65d41a7689", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", + "md5": "ed3a63cc79137f316ee386cd7aaea7e6", + "folder": "charset_normalizer", + } + else: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/e0/d2/e9bff9fb359226c25cda3538f664f54f2804f4b37b0d7c944639e1a51f69/lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", + "md5": "ecfccadd587adb67ca54a24977e1a436", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", + "md5": "9bdbf872c3bdbcb7191d5cdf3176c38a", + "folder": "charset_normalizer", + } + + for package, details in packages.items(): + parsed_url = urlparse(details["url"]) + path = unquote(parsed_url.path) + filename = os.path.basename(path) + + if details["url"].endswith(".whl"): + file_format = "whl" + folder_name = "-".join(filename.split("-")[:2]) + elif details["url"].endswith(".tar.gz"): + file_format = "tar.gz" + folder_name = filename.rsplit(".", 2)[0] + package_path = os.path.join(install_path, f"{package}.{file_format}") + package_path = Path(f"{install_path}/{package}.{file_format}") + package_source_dir = Path(f"{install_path}/{details['folder']}") + + if not Path(f"{package_source_dir}/__init__.py").exists(): + try: + shutil.rmtree(package_source_dir) + except: + pass + + package_filename, headers = urlretrieve( + details["url"], filename=package_path + ) + + with open(package_filename, "rb") as f: + content = f.read() + assert hashlib.md5(content).hexdigest() == details["md5"] + try: + shutil.unpack_archive(package_filename, install_path, format='gztar') + extracted_package_files = Path(f"{install_path}/{folder_name}") + for root, dirs, files in os.walk(extracted_package_files): + for dir in dirs: + subfolder = os.path.join(root, dir) + if subfolder.endswith(details["folder"]): + shutil.copytree( + subfolder, f"{install_path}/{details['folder']}" + ) + if details["folder"] == "six.py": + shutil.copy( + Path(f"{install_path}/{folder_name}/six.py"), + f"{install_path}/six.py", + ) + try: + shutil.rmtree(Path(f"{install_path}/{folder_name}")) + except FileNotFoundError: + pass + except shutil.ReadError as e: + with zipfile.ZipFile(package_filename, "r") as zip_ref: + zip_ref.extractall(install_path) + try: + # ! Remember to update accordingly + shutil.rmtree(Path(f"{install_path}/lxml-5.3.0.dist-info")) + except: + shutil.rmtree(Path(f"{install_path}/charset_normalizer-3.4.1.dist-info")) # ! + remove(package_path) + else: + return + # Patch to natpmp to work without netifaces + with open(Path(f"{install_path}/natpmp/__init__.py"), "r") as f: + lines = f.readlines() + # Define the new function as a string + new_function = ''' +# Plucked from https://github.com/tenable/upnp_info/blob/d20a1fda8ca4877d61b89fe7126077a3a5f0b322/upnp_info.py#L23 +def get_gateway_addr(): + """ + Returns the gateway ip of the router if upnp service is available + """ + try: + locations = set() + location_regex = re.compile("location:[ ]*(.+)"+ chr(13) + chr(10), re.IGNORECASE) + ssdpDiscover = ( + "M-SEARCH * HTTP/1.1"+ chr(13) + chr(10) + + "HOST: 239.255.255.250:1900"+ chr(13) + chr(10) + + 'MAN: "ssdp:discover"'+ chr(13) + chr(10) + + "MX: 1"+ chr(13) + chr(10) + + "ST: ssdp:all"+ chr(13) + chr(10) + + chr(13) + chr(10) + ) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.sendto(ssdpDiscover.encode("ASCII"), ("239.255.255.250", 1900)) + sock.settimeout(3) + try: + while True: + data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes + location_result = location_regex.search(data.decode("ASCII")) + if location_result and (location_result.group(1) in locations) == False: + locations.add(location_result.group(1)) + except socket.error: + sock.close() + if locations: + for location in locations: + parsed_url = urlparse(location) + if parsed_url.path.endswith("xml"): + gateway_ip_address = parsed_url.netloc.split(':')[0] + return gateway_ip_address + except: + pass + +''' + # Replace the function + lines[224:229] = new_function + lines[21] = "import socket\nimport re\nfrom urllib.parse import urlparse" + + with open(Path(f"{install_path}/natpmp/__init__.py"), "w") as f: + f.writelines(lines) + + add_port_mapping() + + +def play_sound(sound): + with bs.get_foreground_host_activity().context: + bs.getsound(sound).play() + + +accessible_online = None + + +@threaded +def confirm_port(): + global accessible_online + time.sleep(5) + with urlopen("https://legacy.ballistica.net/bsAccessCheck") as resp: + resp = resp.read().decode() + resp = ast.literal_eval(resp) + accessible_online = resp["accessible"] + # return resp["accessible"] + + +@threaded +def add_port_mapping(): + if accessible_online: + return + # Try to add UDP port using NAT-PMP + try: + import socket + import natpmp + from natpmp import NATPMPUnsupportedError, NATPMPNetworkError + + try: + natpmp.map_port( + natpmp.NATPMP_PROTOCOL_UDP, + BS_PORT, + BS_PORT, + 14400, + gateway_ip=natpmp.get_gateway_addr(), + ) + if accessible_online: + babase.screenmessage( + "You are now joinable from the internet", (0.2, 1, 0.2) + ) + babase.pushcall( + babase.Call(play_sound, "shieldUp"), from_other_thread=True + ) + except (NATPMPUnsupportedError, NATPMPNetworkError): + import upnpclient + from upnpclient.soap import SOAPError + from urllib.error import HTTPError + + devices = upnpclient.discover() + + if devices == []: + babase.screenmessage( + "Please enable upnp service on your router", (1.00, 0.15, 0.15) + ) + babase.pushcall( + babase.Call(play_sound, "shieldDown"), from_other_thread=True + ) + return + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # connect() for UDP doesn't send packets + s.connect(("10.0.0.0", 0)) + local_ip = s.getsockname()[0] + s.close() + except: + pass + try: + for upnp_dev in devices: + for service in upnp_dev.services: + if service.name in WAN_SERVICE_NAMES: + try: + result = service.GetSpecificPortMappingEntry( + NewRemoteHost="", + NewExternalPort=BS_PORT, + NewProtocol="UDP", + ) + if result["NewEnabled"] and not accessible_online: + if babase.do_once(): + babase.screenmessage( + "Oops seems like your network doesn't support upnp", + (1.0, 0.15, 0.15), + ) + babase.pushcall( + babase.Call(play_sound, "shieldDown"), + from_other_thread=True, + ) + return + except SOAPError: + if accessible_online: + return + service.AddPortMapping( + NewRemoteHost="0.0.0.0", + NewExternalPort=BS_PORT, + NewProtocol="UDP", + NewInternalPort=BS_PORT, + NewInternalClient=local_ip, + NewEnabled="1", + NewPortMappingDescription="Bombsquad", + NewLeaseDuration=14400, + ) + babase.pushcall( + babase.Call(play_sound, "shieldUp"), + from_other_thread=True, + ) + except (SOAPError, HTTPError, UnicodeDecodeError): + babase.screenmessage( + "You will need to manualy port forward at the router :(" + ) + babase.pushcall(babase.Call(play_sound, "error"), from_other_thread=True,) + except ModuleNotFoundError: + pass + + +# ba_meta export babase.Plugin +class Joinable(babase.Plugin): + def on_app_running(self) -> None: + # try: + confirm_port() + if accessible_online: + return + else: + try: + import upnpclient + add_port_mapping() + except ImportError: + try: + install_path = Path(f"{getcwd()}/ba_data/python") + shutil.rmtree(f"{install_path}/upnpy") + shutil.rmtree(f"{install_path}/natpmp") + except FileNotFoundError: + get_modules() + + def on_app_resume(self) -> None: + confirm_port() + add_port_mapping() diff --git a/plugins/utilities/only_night.py b/plugins/utilities/only_night.py new file mode 100644 index 000000000..922fd02b1 --- /dev/null +++ b/plugins/utilities/only_night.py @@ -0,0 +1,50 @@ +# Ported by brostos to api 8 +# Tool used to make porting easier.(https://github.com/bombsquad-community/baport) +"""Only Night.""" + +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from bascenev1._gameactivity import GameActivity + +if TYPE_CHECKING: + pass + + +# ba_meta export babase.Plugin +class OnlyNight(babase.Plugin): + GameActivity.old_on_transition_in = GameActivity.on_transition_in + + def new_on_transition_in(self) -> None: + self.old_on_transition_in() + gnode = bs.getactivity().globalsnode + if self.map.getname() in [ + "Monkey Face", + "Rampage", + "Roundabout", + "Step Right Up", + "Tip Top", + "Zigzag", + "The Pad", + ]: + gnode.tint = (0.4, 0.4, 0.4) + elif self.map.getname() in [ + "Big G", + "Bridgit", + "Courtyard", + "Crag Castle", + "Doom Shroom", + "Football Stadium", + "Happy Thoughts", + "Hockey Stadium", + ]: + gnode.tint = (0.5, 0.5, 0.5) + else: + gnode.tint = (0.3, 0.3, 0.3) + + GameActivity.on_transition_in = new_on_transition_in diff --git a/plugins/utilities/party_filter.py b/plugins/utilities/party_filter.py new file mode 100644 index 000000000..9487d151c --- /dev/null +++ b/plugins/utilities/party_filter.py @@ -0,0 +1,187 @@ +# Discord : @y.lw + +from __future__ import annotations +from typing import Callable, TypeVar +import babase +import bauiv1 as bui +from bauiv1lib.gather.publictab import PublicGatherTab +import bascenev1 as bs + +# Global state +is_refreshing = True +hide_full = False +hide_empty = False +only_empty = False + +# TypeVars +ClassType = TypeVar('ClassType') +MethodType = TypeVar('MethodType') + + +def override(cls: ClassType) -> Callable[[MethodType], MethodType]: + def decorator(new_method: MethodType) -> MethodType: + method_name = new_method.__code__.co_name + if hasattr(cls, method_name): + setattr(cls, f'_original_{method_name}', getattr(cls, method_name)) + setattr(cls, method_name, new_method) + return new_method + return decorator + +# Enhanced Gather Tab + + +class EnhancedPublicGatherTab(PublicGatherTab): + @override(PublicGatherTab) + def _build_join_tab(self, width: float, height: float) -> None: + self._original__build_join_tab(width, height) + self._open_window_button = bui.buttonwidget( + parent=self._container, + label='Party Filters', + size=(120, 45), + position=(110, height - 115), + on_activate_call=bs.WeakCall(self._open_window)) + + @override(PublicGatherTab) + def _open_window(self) -> None: + c_width, c_height = 600, 400 + uiscale = bui.app.ui_v1.uiscale + scale = 1.8 if uiscale is babase.UIScale.SMALL else 1.55 if uiscale is babase.UIScale.MEDIUM else 1.0 + self.window_root = bui.containerwidget( + scale=scale, + stack_offset=(0, -10) if uiscale is babase.UIScale.SMALL else (0, 15), + size=(c_width, c_height), + color=(0.5, 0.5, 0.5), + transition='in_scale', + on_outside_click_call=bs.WeakCall(self._close_window)) + + v_ = 50 + bui.textwidget( + parent=self.window_root, + size=(0, 0), + h_align='center', + v_align='center', + text='Party Filters Menu', + scale=1.5, + color=(1, 1, 0.7), + maxwidth=c_width * 0.8, + position=(c_width * 0.5, c_height - 60)) + + bui.buttonwidget( + parent=self.window_root, + position=(c_width * 0.1, c_height * 0.8), + size=(60, 60), + scale=0.8, + color=(1, 0.3, 0.3), + label=babase.charstr(babase.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self._close_window) + + v = c_height - 175 + bui.checkboxwidget( + parent=self.window_root, + text='Freeze Party List', + position=(c_height // 2, v), + size=(200, 30), + color=(0.6, 0.6, 0.6), + autoselect=True, + textcolor=(0.8, 0.8, 0.8), + value=not is_refreshing, + on_value_change_call=bs.WeakCall(self._toggle_refresh)) + + v -= v_ + bui.checkboxwidget( + parent=self.window_root, + text='Hide Full Parties', + position=(c_height // 2, v), + size=(200, 30), + autoselect=True, + color=(0.6, 0.6, 0.6), + textcolor=(0.8, 0.8, 0.8), + value=hide_full, + on_value_change_call=bs.WeakCall(self._toggle_full)) + + v -= v_ + self._empty_checkbox = bui.checkboxwidget( + parent=self.window_root, + text='Hide Empty Parties', + position=(c_height // 2, v), + size=(200, 30), + autoselect=True, + color=(0.6, 0.6, 0.6), + textcolor=(0.8, 0.8, 0.8), + value=hide_empty, + on_value_change_call=bs.WeakCall(self._toggle_empty)) + + v -= v_ + self._only_empty_checkbox = bui.checkboxwidget( + parent=self.window_root, + text='Only Empty Parties', + position=(c_height // 2, v), + size=(200, 30), + autoselect=True, + color=(0.6, 0.6, 0.6), + textcolor=(0.8, 0.8, 0.8), + value=only_empty, + on_value_change_call=bs.WeakCall(self._toggle_only_empty)) + + @override(PublicGatherTab) + def _close_window(self) -> None: + bui.getsound('shieldDown').play() + bui.containerwidget(edit=self.window_root, transition='out_scale') + + @override(PublicGatherTab) + def _toggle_refresh(self, _=None) -> None: + global is_refreshing + is_refreshing = not is_refreshing + bui.screenmessage( + f"Refreshing {'Enabled' if is_refreshing else 'Disabled'}", color=(1, 1, 0)) + + @override(PublicGatherTab) + def _toggle_full(self, _=None) -> None: + global hide_full + hide_full = not hide_full + bui.screenmessage(f"{'Hiding' if hide_full else 'Showing'} Full Parties", color=(1, 1, 0)) + self._update_party_rows() + + @override(PublicGatherTab) + def _toggle_empty(self, _=None) -> None: + global hide_empty, only_empty + hide_empty = not hide_empty + if hide_empty: + only_empty = False + bui.screenmessage(f"{'Hiding' if hide_empty else 'Showing'} Empty Parties", color=(1, 1, 0)) + if hide_empty: + bui.checkboxwidget(edit=self._only_empty_checkbox, value=only_empty) + self._update_party_rows() + + @override(PublicGatherTab) + def _toggle_only_empty(self, _=None) -> None: + global only_empty, hide_empty + only_empty = not only_empty + if only_empty: + hide_empty = False + bui.screenmessage( + f"{'Showing Only Empty' if only_empty else 'Showing All'} Parties", color=(1, 1, 0)) + if only_empty: + bui.checkboxwidget(edit=self._empty_checkbox, value=hide_empty) + self._update_party_rows() + + @override(PublicGatherTab) + def _update_party_rows(self) -> None: + self._parties_sorted = list(self._parties.items()) + if is_refreshing: + self._original__update_party_rows() + if hide_full: + self._parties_sorted = [ + p for p in self._parties_sorted if p[1].size < p[1].size_max] + if hide_empty: + self._parties_sorted = [p for p in self._parties_sorted if p[1].size > 0] + if only_empty: + self._parties_sorted = [p for p in self._parties_sorted if p[1].size == 0] + + +# ba_meta require api 9 +# ba_meta export babase.Plugin +class ByYelllow(babase.Plugin): + def on_app_running(self) -> None: + pass # Bruh diff --git a/plugins/utilities/path.py b/plugins/utilities/path.py new file mode 100644 index 000000000..df6a15216 --- /dev/null +++ b/plugins/utilities/path.py @@ -0,0 +1,94 @@ +# Copyright 2025 - Solely by BrotherBoard +# Bug? Feedback? Telegram >> @GalaxyA14user + +""" +Path v1.0 - Where it's going to be. + +Experimental. Path tries to predict the next position of bomb. +Path relies on velocity to operate. +Optionally pass spaz node (holder) to assist prediction. +Feedback is appreciated. +""" + +from babase import Plugin +from bascenev1 import ( + timer as tick, + newnode +) + + +class Path: + def __init__(s, node, holder=None): + if node.body == 'crate': + return + s.node, s.kids = node, [] + s.me = holder + s.spy() + + def spy(s): + n = s.node + if not n.exists(): + [_.delete() for _ in s.kids] + s.kids.clear() + return + + [_.delete() for _ in s.kids] + s.kids.clear() + + ip = n.position + iv = n.velocity + if s.me and s.me.hold_node == n: + mv = s.me.velocity + iv = (iv[0]+mv[0], iv[1]+mv[1], iv[2]+mv[2]) + + dots = 200 + ti = 1.2 + tpd = ti / dots + + tick(0.01, s.spy) + for i in range(dots): + t = i * tpd + px = ip[0] + iv[0] * t + py = ip[1] + iv[1] * t + 0.5 * -24 * t**2 + pz = ip[2] + iv[2] * t + + if py <= 0: + l = newnode( + 'locator', + owner=n, + attrs={ + 'shape': 'circleOutline', + 'size': [1], + 'color': (1, 1, 0), + 'draw_beauty': False, + 'additive': True, + 'position': (px, py, pz) + } + ) + s.kids.append(l) + break + dot_node = newnode( + 'text', + owner=n, + attrs={ + 'text': '.', + 'scale': 0.02, + 'position': (px, py, pz), + 'flatness': 1, + 'in_world': True, + 'color': (1-i*4/dots, 0, 0), + 'shadow': 0 + } + ) + s.kids.append(dot_node) + +# brobord collide grass +# ba_meta require api 9 +# ba_meta export babase.Plugin + + +class byBordd(Plugin): + def __init__(s): + _ = __import__('bascenev1lib').actor.bomb.Bomb + o = _.__init__ + _.__init__ = lambda z, *a, **k: (o(z, *a, **k), Path(z.node))[0] diff --git a/plugins/utilities/plugin_editor.py b/plugins/utilities/plugin_editor.py new file mode 100644 index 000000000..81ac4fc74 --- /dev/null +++ b/plugins/utilities/plugin_editor.py @@ -0,0 +1,758 @@ +# ba_meta require api 8 + +# Made by Vishal / Vishuuu / Vishal338 +# Made for everyone who uses it. +# Let's you edit Plugins which are in your device. +# Doesn't work for workspaces. +# Safety Point: Don't mess with the code +# or you might lose all your plugins. + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING, assert_never + +import babase as ba +import bascenev1 as bs +import bauiv1 as bui +from bauiv1lib import popup +from bauiv1lib.confirm import ConfirmWindow +from bauiv1lib.settings.plugins import PluginWindow, Category +import bauiv1lib.settings.plugins as plugs + +if TYPE_CHECKING: + pass + + +class EditModWindow(bui.Window): + """Window for viewing/editing plugins.""" + + def __init__( + self, + transition: str = 'in_right', + origin_widget: bui.Widget | None = None, + plugin: str | None = None, + view_type: str | None = None, + ): + # pylint: disable=too-many-statements + app = bui.app + + # If they provided an origin-widget, scale up from that. + scale_origin: tuple[float, float] | None + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + assert bui.app.classic is not None + uiscale = bui.app.ui_v1.uiscale + self._width = 870.0 if uiscale is bui.UIScale.SMALL else 670.0 + self._x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 + self._height = ( + 390.0 + if uiscale is bui.UIScale.SMALL + else 450.0 if uiscale is bui.UIScale.MEDIUM else 520.0 + ) + top_extra = 10 if uiscale is bui.UIScale.SMALL else 0 + self._theme = ba.app.config.get('Plugin Editor Theme', 'default') + + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + color=(0.13, 0.13, 0.13) if self._theme == 'dark' else None, + scale=( + 2.06 + if uiscale is bui.UIScale.SMALL + else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 + ), + stack_offset=( + (0, -25) if uiscale is bui.UIScale.SMALL else (0, 0) + ), + ) + ) + + self._scroll_width = self._width - (100 + 2 * self._x_inset) + self._scroll_height = self._height - 115.0 + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 724.0 + + Plugin_Name = 'plugin_editor' + self._plugin = plugin + self._view_type = view_type if view_type is not None else 'view' + self._lines = [] + self._original_lines = [] + self._widgets = [] + self._selected_line_widget: ba.Widget | None = None + self._saving = False + self._shown = False + self._plug_path = ba.env()['python_directory_user'] + self._same_plug = __file__ in (self._plug_path + '\\' + self._plugin + '.py', + self._plug_path + '/' + self._plugin + '.py') + self._plug_exists = os.path.exists(self._plug_path + os.sep + self._plugin + '.py') + + if self._theme == 'dark': + self._tint = bs.get_foreground_host_activity().globalsnode.tint + bs.get_foreground_host_activity().globalsnode.tint = (0.4, 0.4, 0.4) + assert app.classic is not None + if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL: + bui.containerwidget( + edit=self._root_widget, on_cancel_call=self._do_back + ) + self._back_button = None + else: + self._back_button = bui.buttonwidget( + parent=self._root_widget, + position=(53 + self._x_inset, self._height - 60), + size=(140, 60), + scale=0.8, + autoselect=True, + label=bui.Lstr(resource='backText'), + button_type='back', + on_select_call=bui.Call(self._update_line_num), + on_activate_call=bui.Call(self._back_button_clicked) + ) + bui.containerwidget( + edit=self._root_widget, cancel_button=self._back_button + ) + + if not self._same_plug and self._plug_exists: + self._line_num = bui.textwidget( + parent=self._root_widget, + position=( + self._width * (0.25 if bui.app.ui_v1.uiscale is bui.UIScale.SMALL + else 0.175), + self._height - 60), + size=(120, 30), + text='1', + color=(0.75, 0.7, 0.8), + h_align='center', + v_align='center', + selectable=True, + always_highlight=True, + editable=True, + ) + + self._title_text = bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 41), + size=(0, 0), + text=bui.Lstr(value=(self._plugin + '.py')), + color=app.ui_v1.title_color, + maxwidth=170, + h_align='center', + v_align='center', + ) + + if self._same_plug: + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.6), + size=(0, 0), + text=bui.Lstr(value=f'You can\'t {self._view_type} the Editor, you PSYCHO!'), + color=(0.7, 0.8, 0.7), + h_align='center', + v_align='center', + ) + if Plugin_Name != self._plugin: + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + size=(0, 0), + text=bui.Lstr(value='You tryna change the file name? HAHAHA!'), + color=(0.7, 0.8, 0.7), + h_align='center', + v_align='center', + ) + + if self._back_button is not None: + bui.buttonwidget( + edit=self._back_button, + button_type='backSmall', + size=(60, 60), + label=bui.charstr(bui.SpecialChar.BACK), + ) + + if not self._same_plug: + if self._plug_exists: + plug_code = open(self._plug_path + os.sep + self._plugin + '.py', 'r') + self._plug_lines = plug_code.readlines() + plug_code.close() + else: + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.6), + size=(0, 0), + text=bui.Lstr(value='Either this is a Workspace Plugin or I can\'t find this.'), + color=(0.7, 0.8, 0.7), + h_align='center', + v_align='center', + ) + return + self._shown = True + if self._view_type == 'edit': + self._add_line_button = bui.buttonwidget( + parent=self._root_widget, + position=(self._width * 0.5 + 100, self._height - 60), + button_type='square', + size=(30, 30), + label='+', + on_activate_call=bui.Call(self._add_line), + ) + self._remove_line_button = bui.buttonwidget( + parent=self._root_widget, + position=(self._width * 0.5 + 145, self._height - 60), + button_type='square', + size=(30, 30), + label='-', + color=(1, 0, 0), + on_activate_call=bui.Call(self._remove_line), + ) + self._save_button = bui.buttonwidget( + parent=self._root_widget, + position=(self._width * 0.5 + 190, self._height - 60), + size=(80, 30), + label='Save', + on_activate_call=lambda: ConfirmWindow( + action=self._save_button_clicked + ), + ) + bui.widget( + edit=self._save_button, + right_widget=self._save_button, + up_widget=self._save_button + ) + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=(50 + self._x_inset, 50), + simple_culling_v=20.0, + highlight=False, + size=(self._scroll_width, self._scroll_height), + selection_loops_to_parent=True, + claims_left_right=True, + ) + bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) + + self._longest_len = 0 + for line in self._plug_lines: + line = line[0:len(line)-(1 if line[len(line)-1] == '\n' else 0)] + self._lines.append(line) + if self._longest_len < len(line): + self._longest_len = len(line) + self._original_lines = self._lines.copy() + + plug_line_height = 18 + sub_width = self._scroll_width + sub_height = (len(self._lines) + 1) * plug_line_height + self._subcontainer = bui.containerwidget( + parent=self._scrollwidget, + size=(sub_width, sub_height), + background=False, + ) + + bui.widget( + edit=self._back_button, + right_widget=self._line_num, + down_widget=( + self._subcontainer if self._selected_line_widget is None else + self._selected_line_widget + ) + ) + bui.widget( + edit=self._line_num, + up_widget=self._line_num, + down_widget=( + self._subcontainer if self._selected_line_widget is None else + self._selected_line_widget + ) + ) + if self._view_type == 'edit': + bui.widget( + edit=self._add_line_button, + up_widget=self._add_line_button, + down_widget=( + self._subcontainer if self._selected_line_widget is None else + self._selected_line_widget + ) + ) + bui.widget( + edit=self._remove_line_button, + up_widget=self._remove_line_button, + down_widget=( + self._subcontainer if self._selected_line_widget is None else + self._selected_line_widget + ) + ) + + self._show_lines() + self._timer = bui.AppTimer(0.1, self._update_line_num, repeat=True) + + def _show_lines(self) -> None: + + sub_height = (len(self._lines) + 1) * 18 + bui.containerwidget(edit=self._subcontainer, + size=(self._scroll_width, sub_height)) + + self._widgets = [] + y_num = 18 * (len(self._lines) - 1) + 10 + for num, line in enumerate(self._lines): + bui.textwidget( + parent=self._subcontainer, + position=(23, y_num + 9), + size=(0, 0), + scale=0.5, + text=str(num+1), + h_align='right', + v_align='center', + ) + abc = bui.textwidget( + parent=self._subcontainer, + position=(-151, y_num), + size=(880, 18), + color=(1.2, 1.2, 1.2), + scale=0.6, + text=line, + selectable=True, + maxwidth=527, + h_align='left', + v_align='center', + always_highlight=True, + ) + bui.widget( + edit=abc, + left_widget=self._back_button, + right_widget=self._add_line_button if self._view_type == 'edit' else abc, + ) + + def edit_them(obj): + if self._view_type == 'edit': + self._un_editable_all() + try: + bui.textwidget(edit=obj, editable=True) + except: + pass + self._save_code() + self._selected_line_widget = obj + try: + bui.textwidget(edit=self._line_num, + text=str(self._widgets.index(obj)+1) + ) + except: + pass + + bui.textwidget(edit=abc, + on_select_call=bui.Call(edit_them, abc), + on_activate_call=bui.Call(edit_them, abc)) + y_num -= 18 + self._widgets.append(abc) + + def _un_editable_all(self) -> None: + for line in self._widgets: + bui.textwidget(edit=line, editable=False) + + def _remove_line(self) -> None: + if self._selected_line_widget is None: + bui.screenmessage('Line is not selected.') + return + index = self._widgets.index(self._selected_line_widget) + self._lines.pop(index) + if len(self._lines) == 0: + self._lines.append('') + self._clear_scroll_widget() + self._show_lines() + bui.containerwidget( + edit=self._subcontainer, + selected_child=self._widgets[index-1 if index > 0 else 0] + ) + self._selected_line_widget = self._widgets[index-1 if index > 0 else 0] + + def _add_line(self) -> None: + if self._selected_line_widget is None: + bui.screenmessage('Line is not selected.') + return + index = self._widgets.index(self._selected_line_widget) + spaces = '' + for ch in self._lines[index]: + if ch != ' ': + break + spaces += ch + if self._lines[index][-1:] == ':': + spaces += ' ' + self._lines.insert(index+1, spaces) + self._clear_scroll_widget() + self._show_lines() + bui.containerwidget( + edit=self._subcontainer, + selected_child=self._widgets[index+1] + ) + self._selected_line_widget = self._widgets[index+1] + + def _save_code(self) -> None: + for num, widget in enumerate(self._widgets): + self._lines[num] = bui.textwidget(parent=self._subcontainer, query=widget) + + def _save_button_clicked(self) -> None: + if self._saving: + return + self._saving = True + self._save_code() + file = open(self._plug_path + os.sep + self._plugin + '.py', 'w') + file.writelines([line + '\n' for line in self._lines]) + file.close() + bui.screenmessage(self._plugin + '.py Saved') + self._saving = False + self._do_back() + + def _update_line_num(self) -> None: + try: + index = int(bui.textwidget(query=self._line_num)) + if self._widgets[index-1] != self._selected_line_widget: + bui.containerwidget( + edit=self._subcontainer, + selected_child=self._widgets[index-1], + visible_child=self._widgets[index-1] + ) + self._selected_line_widget = self._widgets[index-1] + except (IndexError, ValueError, AttributeError): + pass + + def _clear_scroll_widget(self) -> None: + existing_widgets = self._subcontainer.get_children() + if existing_widgets: + for i in existing_widgets: + i.delete() + + def _back_button_clicked(self) -> None: + if self._view_type == 'edit' and not self._same_plug and self._shown: + ConfirmWindow(action=self._do_back) + else: + self._do_back() + + def _do_back(self) -> None: + # pylint: disable=cyclic-import + from bauiv1lib.settings.plugins import PluginWindow + + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._timer = None + if self._theme == 'dark': + bs.get_foreground_host_activity().globalsnode.tint = self._tint + bui.containerwidget( + edit=self._root_widget, transition=self._transition_out + ) + assert bui.app.classic is not None + bui.app.ui_v1.set_main_menu_window( + PluginWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, + ) + + +class Plugin_Window(PluginWindow): + def __init__( + self, + transition: str = 'in_right', + origin_widget: bui.Widget | None = None, + ): + self._info = [] + self._popup_type = None + PluginWindow.__init__(self, transition, origin_widget) + + def _show_category_options(self) -> None: + self._popup_type = 'category' + PluginWindow._show_category_options(self) + + def _show_plugins(self) -> None: + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + plugspecs = bui.app.plugins.plugin_specs + plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {}) + assert isinstance(plugstates, dict) + + plug_line_height = 50 + sub_width = self._scroll_width + num_enabled = 0 + num_disabled = 0 + + plugspecs_sorted = sorted(plugspecs.items()) + + bui.textwidget( + edit=self._no_plugins_installed_text, + text='', + ) + + for _classpath, plugspec in plugspecs_sorted: + # counting number of enabled and disabled plugins + # plugstate = plugstates.setdefault(plugspec[0], {}) + if plugspec.enabled: + num_enabled += 1 + else: + num_disabled += 1 + + if self._category is Category.ALL: + sub_height = len(plugspecs) * plug_line_height + bui.containerwidget( + edit=self._subcontainer, size=(self._scroll_width, sub_height) + ) + elif self._category is Category.ENABLED: + sub_height = num_enabled * plug_line_height + bui.containerwidget( + edit=self._subcontainer, size=(self._scroll_width, sub_height) + ) + elif self._category is Category.DISABLED: + sub_height = num_disabled * plug_line_height + bui.containerwidget( + edit=self._subcontainer, size=(self._scroll_width, sub_height) + ) + else: + # Make sure we handle all cases. + assert_never(self._category) + + num_shown = 0 + for classpath, plugspec in plugspecs_sorted: + plugin = plugspec.plugin + enabled = plugspec.enabled + + if self._category is Category.ALL: + show = True + elif self._category is Category.ENABLED: + show = enabled + elif self._category is Category.DISABLED: + show = not enabled + else: + assert_never(self._category) + + if not show: + continue + + item_y = sub_height - (num_shown + 1) * plug_line_height + check = bui.checkboxwidget( + parent=self._subcontainer, + text=bui.Lstr(value=classpath), + autoselect=True, + value=enabled, + maxwidth=self._scroll_width - 200, + position=(10, item_y), + size=(self._scroll_width - 40, 50), + on_value_change_call=bui.Call( + self._check_value_changed, plugspec + ), + textcolor=( + (0.8, 0.3, 0.3) + if (plugspec.attempted_load and plugspec.plugin is None) + else ( + (0.6, 0.6, 0.6) + if plugspec.plugin is None + else (0, 1, 0) + ) + ), + ) + # noinspection PyUnresolvedReferences + button = bui.buttonwidget( + parent=self._subcontainer, + label=bui.Lstr(resource='mainMenu.settingsText'), + autoselect=True, + size=(100, 40), + position=(sub_width - 130, item_y + 6), + ) + # noinspection PyUnresolvedReferences + bui.buttonwidget( + edit=button, + on_activate_call=bui.Call( + self._show_menu, + button, + (plugin.show_settings_ui if plugin is not None and plugin.has_settings_ui() else None), + plugin, + classpath + ), + ) + + # Allow getting back to back button. + if num_shown == 0: + bui.widget( + edit=check, + up_widget=self._back_button, + left_widget=self._back_button, + right_widget=button, + ) + bui.widget(edit=button, up_widget=self._back_button) + + # Make sure we scroll all the way to the end when using + # keyboard/button nav. + bui.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40) + num_shown += 1 + + bui.textwidget( + edit=self._num_plugins_text, + text=str(num_shown), + ) + + if num_shown == 0: + bui.textwidget( + edit=self._no_plugins_installed_text, + text=bui.Lstr(resource='noPluginsInstalledText'), + ) + + def _show_menu(self, button, func, plugin, classpath) -> None: + choices = ['view', 'edit', 'settings' + ] if plugin is not None and plugin.has_settings_ui() else ['view', 'edit'] + choices_display = [ + bui.Lstr(value='View Code'), + bui.Lstr(value='Edit Code'), + bui.Lstr(value='Settings'), + ] if plugin is not None and plugin.has_settings_ui() else [ + bui.Lstr(value='View Code'), + bui.Lstr(value='Edit Code') + ] + popup.PopupMenuWindow( + position=button.get_screen_space_center(), + choices=choices, + choices_display=choices_display, + current_choice='settings', + width=100, + delegate=self, + scale=( + 2.3 + if bui.app.ui_v1.uiscale is bui.UIScale.SMALL + else 1.65 if bui.app.ui_v1.uiscale is bui.UIScale.MEDIUM else 1.23 + ), + ) + self._info = [button, func, classpath] + self._popup_type = 'options' + + def popup_menu_selected_choice( + self, popup_window: popup.PopupMenuWindow, choice: str + ) -> None: + """Called when a choice is selected in the popup.""" + del popup_window # unused + if self._popup_type == 'category': + self._category = Category(choice) + self._clear_scroll_widget() + self._show_plugins() + bui.buttonwidget( + edit=self._category_button, + label=bui.Lstr(resource=self._category.resource), + ) + elif self._popup_type == 'options': + self._abc(self._info[0], self._info[1], self._info[2], choice) + + def _abc(self, button, func, plugin, view_mode: str) -> None: + # pylint: disable=cyclic-import + if view_mode == 'view' or view_mode == 'edit': + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + + self._save_state() + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.ui_v1.set_main_menu_window( + EditModWindow( + transition='in_right', + plugin=plugin.split('.')[0], + view_type=view_mode + ).get_root_widget(), + from_window=self._root_widget, + ) + elif view_mode == 'settings': + func(button) + + +class PluginEditorSettingsWindow(popup.PopupWindow): + def __init__(self, origin_widget): + self.scale_origin = origin_widget.get_screen_space_center() + bui.getsound('swish').play() + _uiscale = bui.app.ui_v1.uiscale + s = 1.65 if _uiscale is ba.UIScale.SMALL else 1.39 if _uiscale is ba.UIScale.MEDIUM else 1.67 + width = 400 * s + height = width * 0.5 + color = (1, 1, 1) + text_scale = 0.7 * s + self._transition_out = 'out_scale' + transition = 'in_scale' + + self._root_widget = bui.containerwidget(size=(width, height), + on_outside_click_call=self._back, + transition=transition, + scale=(1.5 if _uiscale is ba.UIScale.SMALL else 1.5 + if _uiscale is ba.UIScale.MEDIUM else 1.0), + scale_origin_stack_offset=self.scale_origin) + + bui.textwidget(parent=self._root_widget, + position=(width * 0.49, height * 0.87), size=(0, 0), + h_align='center', v_align='center', text='Plugin Editor Settings', + scale=text_scale * 1.25, color=bui.app.ui_v1.title_color, + maxwidth=width * 0.9) + + back_button = bui.buttonwidget( + parent=self._root_widget, + position=(width * 0.1, height * 0.8), + size=(60, 60), + scale=0.8, + label=ba.charstr(ba.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self._back) + + bui.containerwidget(edit=self._root_widget, cancel_button=back_button) + + bui.textwidget(parent=self._root_widget, + position=(width * 0.32, height * 0.5), + size=(0, 0), + h_align='center', + v_align='center', + text='Theme :', + scale=text_scale * 1.15, + color=bui.app.ui_v1.title_color + ) + + popup.PopupMenu( + parent=self._root_widget, + position=(width * 0.42, height * 0.5 - 30), + button_size=(200.0, 60.0), + width=100.0, + choices=[ + 'default', + 'dark', + ], + choices_display=[ + bui.Lstr(value='Default'), + bui.Lstr(value='Dark'), + ], + current_choice=ba.app.config.get('Plugin Editor Theme', 'default'), + on_value_change_call=self._set_theme, + ) + + def _set_theme(self, val: str) -> None: + if ba.app.config.get('Plugin Editor Theme', 'default') != val: + cfg = bui.app.config + cfg['Plugin Editor Theme'] = val + cfg.apply_and_commit() + bui.screenmessage( + bui.Lstr(value=f'Editor Theme changed to {val.title()}'), + color=(0.0, 1.0, 0.0), + ) + + def _back(self) -> None: + bui.getsound('swish').play() + bui.containerwidget(edit=self._root_widget, transition='out_scale') + + +# ba_meta export plugin +class ByVishuuu(ba.Plugin): + """A plugin to edit plugins.""" + + def __init__(self) -> None: + plugs.PluginWindow = Plugin_Window + + def has_settings_ui(self): + return True + + def show_settings_ui(self, source_widget): + PluginEditorSettingsWindow(source_widget) diff --git a/plugins/utilities/plugtools.py b/plugins/utilities/plugtools.py new file mode 100644 index 000000000..58de52a48 --- /dev/null +++ b/plugins/utilities/plugtools.py @@ -0,0 +1,521 @@ +# Copyright 2025 - Solely by BrotherBoard +# Intended for personal use only +# Bug? Feedback? Telegram >> @BroBordd + +""" +PlugTools v1.5 - Live Plugin Action + +Beta. Feedback is appreciated. +Adds a dev console tab for plugin management. + +Features vary between: +- Dynamic Control: Enables immediate loading and reloading of plugins. +- Real-time Monitoring: Reports status of plugin files (new, modified, deleted). +- Plugin Overview: Displays operational state (enabled/disabled) and integrity (original/modified). +- Plugin Data: Provides file path, size, timestamps, and code structure analysis. +- Navigation: Offers controls to browse the plugin list. +- Logging: Has a built-in log display with proper indentation. +""" + +from os.path import ( + splitext, + getmtime, + getctime, + basename, + getsize, + isfile, + exists, + join +) +from os import ( + scandir, + access, + R_OK, + stat +) +from babase import ( + PluginSpec, + Plugin, + Call, + env, + app +) +from babase._devconsole import ( + DevConsoleTabEntry as ENT, + DevConsoleTab as TAB +) +from bauiv1 import ( + get_string_width as sw, + SpecialChar as sc, + charstr as cs, + apptimer as teck, + screenmessage as push, + getsound as gs +) +from traceback import format_exc as ERR +from datetime import datetime +from importlib import reload +from typing import override +from sys import modules +from gc import collect +from ast import ( + FunctionDef, + ImportFrom, + Attribute, + ClassDef, + Import, + parse, + walk, + Name +) + + +class PlugTools(TAB): + KEY = 'PT_BY' + + def __init__(s): + s.bys = META() + s.bad = [] + s.logs = 'No errors' + s.mem = {_: MT(_) for _ in s.bys} + s.eye = look() + s.e = False + s.spy() + + def spy(s): + b = 0 + for _ in s.bys.copy(): + if not exists(PAT(_)): + s.bys.remove(_) + push(f'Plugin {_} suddenly disappeared!\nAnd so, was removed from list.', color=(1, 1, 0)) + gs('block').play() + s.eye = look() + if s.hl() == _: + s.hl(None) + b = 1 + sp = app.plugins.plugin_specs.get(_, 0) + if not sp: + continue + p = app.plugins + if getattr(sp, 'enabled', False): + o = s.sp.plugin + if o in p.active_plugins: + p.active_plugins.remove(o) + if o in p.plugin_specs: + p.plugin_specs.pop(o) + del s.sp.plugin, o + collect() + try: + reload(modules[NAM(_, 0)]) + except: + pass + continue + if MT(_) != s.mem[_] and _ not in s.bad: + s.bad.append(_) + push(f'Plugin {_} was modified!\nSee if you want to take action.', color=(1, 1, 0)) + gs('dingSmall').play() + b = 1 + if hasattr(s, 'sp'): + e = getattr(s.sp, 'enabled', False) + if e != s.e: + s.e = e + b = 1 + eye = look() + s1 = set(s.eye) + s2 = set(eye) + df = list(s2-s1) + nu = [] + if df: + for dd in df: + try: + _ = kang(dd) + except: + eye.remove(dd) + continue + nu.append(_) + s.bys.append(_) + s.mem[_] = 0 + s.bad.append(_) + s.eye = eye + b = 1 + if nu: + l = len(nu) + push(f"Found {l} new plugin{['s', ''][l == 1]}:\n{', '.join(nu)}\nSee what to do with {['it', 'them'][l != 1]}", color=( + 1, 1, 0)) + gs('dingSmallHigh').play() + if b: + try: + s.request_refresh() + except RuntimeError: + pass + teck(0.1, s.spy) + + @override + def refresh(s): + # Preload + by = s.hl() + if by not in s.bys: + by = None + s.hl(None) + s.by = by + s.sp = app.plugins.plugin_specs.get(by, 0) if by else 0 + s.i = getattr(s, 'i', 0 if by is None else s.bys.index(by)//10) + # UI + w = s.width + x = -w/2 + z = x+w + # Bools + e = s.e = getattr(s.sp, 'enabled', False) + m = by in s.bad + d = by is None + # Buttons + sx = w*0.2 + mx = sx*0.98 + z -= sx + s.button( + 'Metadata', + pos=(z, 50), + size=(mx, 43), + call=s.metadata, + disabled=d + ) + s.button( + ['Load', 'Reload'][e], + pos=(z, 5), + size=(mx, 43), + call=s._load, + disabled=d + ) + # Separator + s.button( + '', + pos=(z-(w*0.006), 5), + size=(2, 90) + ) + # Plugin info + sx = w*0.1 + z -= sx + az = z+sx/2.23 + t = 'Entry' if d else by + tw = GSW(t) + mx = sx*0.9 + s.text( + t, + pos=(az, 80), + scale=1 if tw < mx else mx/tw, + ) + t = 'State' if d else ['Disabled', 'Enabled'][e] + tw = GSW(t) + s.text( + t, + pos=(az, 50), + scale=1 if tw < mx else mx/tw, + ) + t = 'Purity' if d else ['Original', 'Modified'][m] + tw = GSW(t) + s.text( + t, + pos=(az, 20), + scale=1 if tw < mx else mx/tw, + ) + # Separator + s.button( + '', + pos=(z-(w*0.0075), 5), + size=(2, 90) + ) + # Next + sx = w*0.03 + mx = sx*0.6 + z -= sx + s.button( + cs(sc.RIGHT_ARROW), + pos=(z, 5), + size=(mx, 90), + call=s.next, + disabled=(s.i+1)*10 > len(s.bys) + ) + # Plugins + sx = w*0.645/5 + mx = sx*0.99 + zx = mx*0.9 + z -= sx*5 + for i in range(5): + for j in range(2): + k = j*5+i+s.i*10 + if k >= len(s.bys): + break + t = s.bys[k] + tw = GSW(t) + s.button( + t, + size=(mx, 43), + pos=(z+sx*i, 50-45*j), + label_scale=1 if tw < zx else zx/tw, + call=Call(s.hl, t), + style=[['blue', 'blue_bright'], ['purple', 'purple_bright']][t in s.bad][t == by] + ) + # Prev + sx = w*0.03 + mx = sx*0.6 + z -= sx*0.7 + s.button( + cs(sc.LEFT_ARROW), + pos=(z, 5), + size=(mx, 90), + call=s.prev, + disabled=s.i == 0 + ) + if s.height <= 100: + return + # Expanded logs + t = s.logs + h = 25 + pos = (x+10, s.height) + z = len(t) + p = list(pos) + m = max(t.replace('\\n', '') or [''], key=GSW) + l = GSW(str(m))/1.2 + ln = t.split('\\n') + mm = max(ln, key=GSW) + sk = 0.8 + ml = (s.height-100) * 0.04 + ww = (l*sk)*len(mm) + sk = sk if ww < s.width else (s.width*0.98/ww)*sk + zz = len(ln) + sk = sk if zz <= ml else (ml/zz)*sk + xf = 0 + for i in range(z): + p[0] += [l*sk, 0][i == 0] + if xf: + xf = 0 + continue + j = t[i] + k = t[i+1] if (i+1) < z else j + if j == '\\' and k == 'n': + p[0] = pos[0]-(l*1.5)*sk + p[1] -= h*(sk*1.28) + xf = 1 + continue + s.text( + j, + pos=tuple(p), + h_align='center', + v_align='top', + scale=sk + ) + + def hl(s, i=None): + i and deek() + c = app.config + if i is None: + return c.get(s.KEY, None) + c[s.KEY] = i + c.commit() + s.request_refresh() + + def _load(s): + h = ['load', 'reload'][s.e] + ex, er = s.load() + if ex: + k = f': {ex}' if str(ex).strip() else '' + j = f'Error {h}ing {s.by}' + push(f'{j}{k}\nExpand dev console to see more.\nTraceback dumped to terminal too.', color=(1, 0, 0)) + gs('error').play() + m = j+':\n'+er + print('[PlugTools] '+m) + s.logs = m.replace('\n', '\\n') + s.request_refresh() + return + s.logs = 'No errors' + if ex is False: + return + push(h.title()+'ed '+s.by, color=(0, 1, 0)) + gs('gunCocking').play() + s.request_refresh() + + def load(s): + _ = s.by + if _ in s.bad: + s.bad.remove(_) + s.mem[_] = MT(_) + p = app.plugins + if s.e: + if hasattr(s.sp, 'plugin'): + o = s.sp.plugin + if o in p.active_plugins: + p.active_plugins.remove(o) + del s.sp.plugin + collect() + try: + m = reload(modules[NAM(_, 0)]) + except KeyError: + gs('block').play() + push(f"{s.by} is malformed!\nAre you sure there's no errors?", color=(1, 1, 0)) + return (False, 0) + except Exception as ex: + return (ex, ERR()) + else: + m = __import__(NAM(_, 0)) + try: + cls = getattr(m, _.split('.', 1)[1]) + except Exception as ex: + return (ex, ERR()) + try: + ins = cls() + except Exception as ex: + return (ex, ERR()) + try: + ins.on_app_running() + except Exception as ex: + return (ex, ERR()) + s.sp = PluginSpec(class_path=_, loadable=True) + s.sp.enabled = True + s.sp.plugin = ins + p.plugin_specs[_] = s.sp + p.active_plugins.append(ins) + return (0, 0) + + def metadata(s): + f = PAT(s.sp.class_path) + info = [] + if exists(f): + info.append(f'File Path: {f}') + info.append("File Exists: Yes") + info.append(f"File Size: {getsize(f)} bytes") + try: + with open(f, 'r', encoding='utf-8', errors='ignore') as file: + lines = file.readlines() + content = "".join(lines) # Read entire content for AST parsing and char count + line_count = len(lines) + char_count = len(content) + + info.append(f"Line Count: {line_count}") + info.append(f"Character Count: {char_count}") + + # Python specific programmatic analysis + function_count = 0 + class_count = 0 + import_statement_count = 0 + comment_lines = 0 + blank_lines = 0 + + try: + tree = parse(content) # Use parse directly + for node in walk(tree): # Use walk directly + if isinstance(node, FunctionDef): # Use FunctionDef directly + function_count += 1 + elif isinstance(node, ClassDef): # Use ClassDef directly + class_count += 1 + elif isinstance(node, (Import, ImportFrom)): # Use Import, ImportFrom directly + import_statement_count += 1 + # Iterate through physical lines for comments and blank lines + for line in lines: + stripped_line = line.strip() + if not stripped_line: + blank_lines += 1 + elif stripped_line.startswith('#'): + comment_lines += 1 + info.append(f"Function Definitions: {function_count}") + info.append(f"Class Definitions: {class_count}") + info.append(f"Import Statements: {import_statement_count}") + info.append(f"Comment Lines: {comment_lines}") + info.append(f"Blank Lines: {blank_lines}") + + except SyntaxError as se: + info.append(f"Python Syntax Error: {se}") + except Exception as ast_e: + info.append(f"Error analyzing Python file structure: {ast_e}") + + except Exception as e: + info.append(f"Could not read file content for analysis: {e}") + + creation_time = datetime.fromtimestamp(getctime(f)) + info.append(f"Creation Time: {creation_time}") + + mod_time = datetime.fromtimestamp(getmtime(f)) + info.append(f"Last Modified: {mod_time}") + else: + info.append(f'File Path: {f}') + info.append("File Exists: No") + push('\n'.join(info)) + gs('powerup01').play() + + def next(s): + deek() + s.i += 1 + s.request_refresh() + + def prev(s): + deek() + s.i -= 1 + s.request_refresh() + + +def MT(_): return stat(PAT(_)) +def GSW(s): return sw(s, suppress_warning=True) + + +def NAM(_, py=1): return _.split('.', 1)[0]+['', '.py'][py] +def PAT(_): return join(ROOT, NAM(_)) + + +ROOT = env()['python_directory_user'] +def META(): return app.meta.scanresults.exports_by_name('babase.Plugin') + + +def look(): + python_files = [] + try: + with scandir(ROOT) as entries: + for entry in entries: + if entry.is_file() and entry.name.endswith(".py"): + if access(entry.path, R_OK): + python_files.append(entry.path) + except FileNotFoundError: + pass + except PermissionError: + pass + return python_files + + +def kang(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + source_code = f.read() + + tree = parse(source_code) + lines = source_code.splitlines() + export_line_num = -1 + for i, line in enumerate(lines): + if line.strip() == '# ba_meta export babase.Plugin': + export_line_num = i + 1 + break + if export_line_num == -1: + return None + + filename_without_ext = splitext(basename(file_path))[0] + for node in tree.body: + if isinstance(node, ClassDef): + if node.lineno > export_line_num: + for base in node.bases: + if (isinstance(base, Name) and base.id == 'Plugin') or \ + (isinstance(base, Attribute) and base.attr == 'Plugin' and isinstance(base.value, Name) and base.value.id == 'babase'): + return f"{filename_without_ext}.{node.name}" + return None + + +def deek(): return gs('deek').play() + +# brobord collide grass +# ba_meta require api 9 +# ba_meta export babase.Plugin + + +class byBordd(Plugin): + def __init__(s): + C = PlugTools + N = C.__name__ + E = ENT(N, C) + I = app.devconsole + I.tabs = [_ for _ in I.tabs if _.name != N]+[E] + I._tab_instances[N] = E.factory() diff --git a/plugins/utilities/power.py b/plugins/utilities/power.py new file mode 100644 index 000000000..8fa13d607 --- /dev/null +++ b/plugins/utilities/power.py @@ -0,0 +1,853 @@ +# Copyright 2025 - Solely by BrotherBoard +# Intended for personal use only +# Bug? Feedback? Telegram >> @GalaxyA14user + +""" +Power v2.7 - With one click + +Experimental. Feedback is appreciated. +Adds a dev console tab with some features I find useful. +Power is mainly focused on the multiplayer side. +Can be considered a good tool to have around. +""" + +from datetime import datetime as DT +from typing import override +from babase import ( + clipboard_is_supported as CIS, + clipboard_set_text as CST, + Plugin, + app +) +from babase._devconsole import ( + DevConsoleTabEntry as ENT, + DevConsoleTab as TAB +) +from bascenev1 import ( + get_connection_to_host_info_2 as HOST, + disconnect_from_host as LEAVE, + disconnect_client as DISC, + broadcastmessage as push, + get_chat_messages as GCM, + connect_to_party as CON, + get_game_roster as ROST, + chatmessage as chat +) +from bauiv1 import ( + get_string_width as sw, + SpecialChar as sc, + apptimer as teck, + charstr as cs, + Call +) + + +class Power(TAB): + def __init__(s): + s.j = [None, None, None] + s.ji = 1 + [setattr(s, _, None) for _ in 'cpnh'] + [setattr(s, _, {}) for _ in ['rr', 'hi']] + [setattr(s, _, []) for _ in ['cm', 'og', 'r', 'ls']] + [setattr(s, _, 0) for _ in ['ii', 'eii', 'ci', 're', 'ri', 'eri', 'li', 'lii']] + teck(3, s.spy) + + def rf(s): + try: + s.request_refresh() + except RuntimeError: + pass + + def spy(s): + _ = 0 + r = ROST() + if r != s.r: + s.rr = {i['display_string']: (i['client_id'], i['players']) for i in r} + s.r = r + _ = 1 + h = HOST() + if h != s.h: + s.ri = 0 + s.h = h + _ = 1 + t = getattr(s.h, 'name', 'Not in a server') + a = getattr(s.h, 'address', '127.0.0.1') + p = getattr(s.h, 'port', '43210') + if s.h: + tt = t if t.strip() else '...' + if t.strip() or not any(key[1] == a for key in s.hi): + s.hi[(tt, a)] = (tt, p) + if tt != '...': + if ('...', a) in s.hi: + del s.hi[('...', a)] + ng = GCM() + if s.og != ng: + s.og = ng + ls = ng[-1] + ch = s.cm[0][1] if len(s.cm) else 0 + if ch and ls == s.cm[0][0]: + s.cm[0] = (ls, ch+1) + else: + s.cm.insert(0, (ls, 1)) + if s.ci: + s.ci += 1 + _ = 1 + _ and s.rf() + teck(0.1, s.spy) + + @override + def refresh(s): + sf = s.width / 1605.3 + zf = s.height / 648 + x = -s.width/2 + T, B = s.text, s.button + if len(s.r) and s.ri >= len(s.r): + s.ri = len(s.r) - 1 + if len(s.r) and s.eri >= len(s.r): + s.eri = len(s.r) - 1 + if s.j[0] == 'JRejoin' and s.ji <= s.re: + s.ji = s.re + 1 + push('Job time cannot be less than rejoin time\nwhen job is JRejoin. Updated job time to ' + + str(s.ji), color=(1, 1, 0)) + if s.height > 100: + B( + cs(sc.UP_ARROW), + pos=(x + 10 * sf, 606*zf), + size=(280*sf, 35*zf), + disabled=s.eri <= 0, + call=Call(s.mv, 'eri', -1) + ) + B( + cs(sc.DOWN_ARROW), + pos=(x + 10 * sf, 290*zf), + size=(280*sf, 35*zf), + disabled=s.eri >= len(s.r)-7, + call=Call(s.mv, 'eri', 1) + ) + nt = "No roster detected\nJoin some public party" + w = GSW(nt) + 0 if len(s.r) else T( + nt, + pos=(x + 150 * sf, 495*zf), + h_align='center', + v_align='top', + scale=1 if w < (290*sf) else (290*sf)/w + ) + for i, z in enumerate(s.rr.items()): + if i < s.eri: + continue + if i >= (s.eri+7): + break + n, g = z + c, p = g + w = GSW(n) + B( + n, + size=(280 * sf, 37*zf), + pos=(x + 10 * sf, (564-39*(i-s.eri))*zf), + style=[['blue', 'blue_bright'], ['purple', 'purple_bright']][not p][s.c == c], + call=Call(s.prv, c, p, n), + label_scale=1 if w < 280 * sf else (280 * sf)/w + ) + B( + '', + size=(280 * sf, 2), + pos=(x + 10 * sf, 280*zf), + style='bright' + ) + bb = s.c is None + B( + 'Bomb' if bb else (['Client', 'Host'][s.c == -1]+f' {s.c}'), + pos=(x + 10 * sf, 230*zf), + size=(280 * sf, 40*zf), + disabled=bb, + call=Call(push, str(s.n)) + ) + B( + 'Mention', + size=(280 * sf, 40*zf), + pos=(x + 10 * sf, 185*zf), + call=Call(chat, str(s.n)), + disabled=bb + ) + B( + 'Players', + size=(280 * sf, 40*zf), + pos=(x + 10 * sf, 140*zf), + call=Call(push, '\n'.join( + [' '.join([f'{i}={j}' for i, j in _.items()]) for _ in s.p]) if s.p else ''), + disabled=bb or (not s.p) + ) + B( + 'Kick', + size=(280 * sf, 40*zf), + pos=(x + 10 * sf, 95*zf), + call=Call(KICK, lambda: s.rr[s.n][0]), + disabled=bb or (s.c == -1) + ) + B( + 'JKick', + size=(280 * sf, 40*zf), + pos=(x + 10 * sf, 50*zf), + call=Call(s.job, Call(KICK, lambda: s.rr[s.n][0]), ['JKick', s.c, s.n]), + disabled=bb or (s.c == -1) + ) + B( + 'Vote', + size=(280 * sf, 40*zf), + pos=(x + 10 * sf, 5*zf), + call=Call(chat, '1'), + disabled=not s.r + ) + B( + '', + size=(2, 635*zf), + pos=(x + 300 * sf, 5*zf), + style='bright' + ) + t = getattr(s.h, 'name', 'Not in a server') + a = getattr(s.h, 'address', '127.0.0.1') + p = getattr(s.h, 'port', '43210') + w = GSW(t) + B( + t if t.strip() else 'Loading...', + size=(400 * sf, 35*zf), + pos=(x + 311 * sf, 606*zf), + disabled=not s.h, + label_scale=1 if w < 390 * sf else (390 * sf)/w, + call=Call(push, f"{t}\nHosted on build {getattr(s.h, 'build_number', '0')}" if t.strip( + ) else 'Server is still loading...\nIf it remains stuck on this\nthen either party is full, or a network issue.'), + ) + w = GSW(a) + B( + a, + size=(300 * sf, 35*zf), + pos=(x + 311 * sf, 568*zf), + call=Call(COPY, a), + disabled=not s.h, + label_scale=1 if w < 290 * sf else (290 * sf)/w + ) + w = GSW(str(p)) + B( + str(p), + size=(97 * sf, 35*zf), + pos=(x + 614 * sf, 568*zf), + disabled=not s.h, + call=Call(COPY, str(p)), + label_scale=1 if w < 90 * sf else (90 * sf)/w + ) + B( + 'Leave', + size=(400 * sf, 35*zf), + pos=(x + 311 * sf, 530*zf), + call=LEAVE, + disabled=not s.h + ) + B( + 'Rejoin', + size=(200 * sf, 35*zf), + pos=(x + 311 * sf, 492*zf), + call=Call(REJOIN, a, p, lambda: s.re), + disabled=not s.h + ) + B( + 'JRejoin', + size=(197 * sf, 35*zf), + pos=(x + 514 * sf, 492*zf), + call=Call(s.job, Call(REJOIN, a, p, lambda: s.re), ['JRejoin', a, str(p)]), + disabled=not s.h + ) + B( + '+', + size=(131 * sf, 35*zf), + pos=(x + 579 * sf, 454*zf), + call=Call(s.mv, 're', 1) + ) + B( + str(s.re or 0.1), + size=(131 * sf, 35*zf), + pos=(x + 444 * sf, 454*zf), + call=Call( + push, f"Rejoins after {s.re or 0.1} second{['', 's'][s.re != 1]}\nKeep this 0.1 unless server kicks fast rejoins\nLife in server = job time - rejoin time") + ) + B( + '-', + size=(131 * sf, 35*zf), + pos=(x + 311 * sf, 454*zf), + disabled=s.re <= 0.5, + call=Call(s.mv, 're', -1) + ) + B( + '', + size=(2, 635*zf), + pos=(x + 720 * sf, 5*zf), + style='bright' + ) + B( + '', + size=(400 * sf, 2), + pos=(x + 311 * sf, 445*zf), + style='bright' + ) + for i, e in enumerate(s.hi.items()): + if i < s.eii: + continue + if i >= (s.eii+9): + break + g, v = e + _, a = g + n, p = v + w = GSW(n) + B( + n, + size=(400 * sf, 37*zf), + pos=(x + 311 * sf, (358-39*(i-s.eii))*zf), + label_scale=1 if w < 290 * sf else (290 * sf)/w, + call=Call(JOIN, a, p, False), + disabled=n == '...' + ) + nt = "Server join history\nServers you join are saved here" + w = GSW(nt) + 0 if len(s.hi) else T( + nt, + pos=(x + 510 * sf, 265*zf), + v_align='top', + scale=1 if w < (380*sf) else (380*sf)/w + ) + B( + cs(sc.DOWN_ARROW), + pos=(x + 311 * sf, 8*zf), + size=(398*sf, 35*zf), + disabled=s.eii >= len(s.hi)-9, + call=Call(s.mv, 'eii', 1) + ) + B( + cs(sc.UP_ARROW), + pos=(x + 311 * sf, 400*zf), + size=(400*sf, 35*zf), + disabled=s.eii <= 0, + call=Call(s.mv, 'eii', -1) + ) + bb = s.j[0] is None + B( + 'No job' if bb else 'Job', + size=(300 * sf, 35*zf), + pos=(x + 727 * sf, 606*zf), + call=Call(push, s.j[0]), + disabled=bb + ) + w = 0 if bb else GSW(str(s.j[1])) + B( + 'Target' if bb else str(s.j[1]), + size=(300 * sf, 35*zf), + pos=(x + 727 * sf, 568*zf), + call=Call(push, s.j[2]), + disabled=bb, + label_scale=1 if w < 110 * sf else (110 * sf)/w + ) + B( + 'Stop', + size=(300 * sf, 35*zf), + pos=(x + 727 * sf, 530*zf), + call=Call(s.job, None, [None, None, None]), + disabled=bb + ) + B( + '+', + size=(96 * sf, 35*zf), + pos=(x + 931 * sf, 492*zf), + call=Call(s.mv, 'ji', 1) + ) + B( + str(s.ji or 0.1), + size=(100 * sf, 35*zf), + pos=(x + 828 * sf, 492*zf), + call=Call(push, f"Job runs every {s.ji or 0.1} second{['', 's'][s.ji != 1]}") + ) + B( + '-', + size=(98 * sf, 35*zf), + pos=(x + 727 * sf, 492*zf), + disabled=s.ji <= 0.5, + call=Call(s.mv, 'ji', -1) + ) + B( + 'Power', + size=(300 * sf, 35*zf), + pos=(x + 727 * sf, 454*zf), + call=Call(push, 'Power v2.5 FullUI\nCollapse dev console to switch to MinUI') + ) + B( + '', + size=(300 * sf, 2), + pos=(x + 727 * sf, 445*zf), + style='bright' + ) + B( + '', + size=(2, 635*zf), + pos=(x + 1034 * sf, 5*zf), + style='bright' + ) + 0 if len(s.cm) else T( + 'Chat is still empty.\nHurry up and fill it with nonesense', + pos=(x+1320 * sf, 330 * zf) + ) + for i, g in enumerate(s.cm): + if i < s.ci: + continue + if i >= s.ci+15: + break + i = i - s.ci + m, _ = g + sn, ms = m.split(': ', 1) + w = GSW(sn) + w = [w, 30*sf][w < 30*sf] + s1 = [w, 200*sf][w > 200*sf] + B( + sn, + size=(s1, 35*zf), + pos=(x + 1040*sf, (48+37*i)*zf), + style='purple', + label_scale=1 if w < (s1-10*sf) else (s1-10*sf)/w, + call=Call(s.chk, sn) + ) + s2 = 555*sf - s1 - 53*(_ > 1) + B( + '', + size=(s2, 35*zf), + pos=(x + 1045*sf+s1, (48+37*i)*zf), + style='black' + ) + w = GSW(ms) + T( + ms, + pos=(x + s1+(1050)*sf, (48+17+37*i)*zf), + scale=1 if w < (s2-10*sf) else (s2-10*sf)/w, + h_align='left' + ) + z = f'x{_}' + w = GSW(z) + _ > 1 and B( + z, + pos=(x+s1+s2+(1050)*sf, (48+37*i)*zf), + size=(50*sf, 35*zf), + label_scale=1 if w < (40*sf) else (40*sf)/w, + style='yellow_bright' + ) + B( + cs(sc.DOWN_ARROW), + pos=(x+1042*sf, 8*zf), + size=(555*sf, 35*zf), + call=Call(s.mv, 'ci', -1), + disabled=s.ci <= 0 or not s.cm + ) + B( + cs(sc.UP_ARROW), + pos=(x+1042*sf, 606*zf), + size=(555*sf, 35*zf), + call=Call(s.mv, 'ci', 1), + disabled=(s.ci >= len(s.cm)-15) or not s.cm + ) + B( + cs(sc.DOWN_ARROW), + pos=(x+727*sf, 8*zf), + size=(300*sf, 35*zf), + disabled=(s.li >= len(s.ls)-16) or not s.ls, + call=Call(s.mv, 'li', 1) + ) + B( + cs(sc.UP_ARROW), + pos=(x+727*sf, 400*zf), + size=(300*sf, 35*zf), + disabled=s.li <= 0, + call=Call(s.mv, 'li', -1) + ) + 0 if s.ls else T( + 'Job logs here\nLike you even care', + pos=(x+875*sf, 232*zf) + ) + for _, g in enumerate(s.ls): + if _ < s.li: + continue + if _ >= s.li+16: + break + _ = _ - s.li + l, t = g + B( + '', + pos=(x+727*sf, (376-_*22)*zf), + size=(300*sf, 20*zf), + label_scale=0.7, + corner_radius=0, + style='black', + call=Call(push, t) + ) + T( + l, + pos=(x+732*sf, (386-_*22)*zf), + scale=0.6, + h_align='left' + ) + else: + B( + cs(sc.DOWN_ARROW), + pos=(x + 10 * sf, 10), + size=(30 * sf, s.height-17), + disabled=(s.ri >= len(s.r)-3) or not s.r, + call=Call(s.mv, 'ri', 1) + ) + B( + cs(sc.UP_ARROW), + pos=(x + 250 * sf, 10), + size=(30 * sf, s.height-17), + disabled=(s.ri <= 0) or not s.r, + call=Call(s.mv, 'ri', -1) + ) + nt = "No roster\nYou're alone" + w = GSW(nt) + 0 if len(s.r) else T( + nt, + pos=(x + 147 * sf, s.height-17), + h_align='center', + v_align='top', + scale=1 if w < (200*sf) else (200*sf)/w + ) + for i, z in enumerate(s.rr.items()): + if i < s.ri: + continue + if i >= (s.ri+3): + break + n, g = z + c, p = g + w = GSW(n) + B( + n, + size=(210 * sf, 27), + pos=(x + 40 * sf, s.height-35-27*(i-s.ri)), + style=[['blue', 'blue_bright'], ['purple', 'purple_bright']][not p][s.c == c], + call=Call(s.prv, c, p, n), + label_scale=1 if w < 200 * sf else (200 * sf)/w + ) + bb = s.c is None + B( + 'Bomb' if bb else (['Client', 'Host'][s.c == -1]+f' {s.c}'), + pos=(x + 287 * sf, s.height-34), + size=(120 * sf, 27), + disabled=bb, + call=Call(push, str(s.n)) + ) + B( + 'Mention', + size=(120 * sf, 27), + pos=(x + 287 * sf, s.height-90), + call=Call(chat, str(s.n)), + disabled=bb + ) + B( + 'Players', + size=(120 * sf, 27), + pos=(x + 287 * sf, s.height-62), + call=Call(push, '\n'.join( + [' '.join([f'{i}={j}' for i, j in _.items()]) for _ in s.p]) if s.p else ''), + disabled=bb or (not s.p) + ) + B( + 'Kick', + size=(120 * sf, 27), + pos=(x + 407 * sf, s.height-34), + call=Call(KICK, lambda: s.rr[s.n][0]), + disabled=bb or (s.c == -1) + ) + B( + 'JKick', + size=(120 * sf, 27), + pos=(x + 407 * sf, s.height-62), + call=Call(s.job, Call(KICK, lambda: s.rr[s.n][0]), ['JKick', s.c, s.n]), + disabled=bb or (s.c == -1) + ) + B( + 'Vote', + size=(120 * sf, 27), + pos=(x + 407 * sf, s.height-90), + call=Call(chat, '1'), + disabled=not s.r + ) + B( + '', + size=(2, s.height-17), + pos=(x + 535 * sf, 10), + style='bright' + ) + bb = s.j[0] is None + B( + 'No job' if bb else 'Job', + size=(120 * sf, 27), + pos=(x + 544 * sf, s.height-34), + call=Call(push, s.j[0]), + disabled=bb + ) + w = 0 if bb else GSW(str(s.j[1])) + B( + 'Target' if bb else str(s.j[1]), + size=(120 * sf, 27), + pos=(x + 544 * sf, s.height-62), + call=Call(push, s.j[2]), + disabled=bb, + label_scale=1 if w < 110 * sf else (110 * sf)/w + ) + B( + 'Stop', + size=(120 * sf, 27), + pos=(x + 544 * sf, s.height-90), + call=Call(s.job, None, [None, None, None]), + disabled=bb + ) + B( + '+', + size=(50 * sf, 27), + pos=(x + 664 * sf, s.height-34), + call=Call(s.mv, 'ji', 1) + ) + B( + str(s.ji or 0.1), + size=(50 * sf, 27), + pos=(x + 664 * sf, s.height-62), + call=Call(push, f"Job runs every {s.ji or 0.1} second{['', 's'][s.ji != 1]}") + ) + B( + '-', + size=(50 * sf, 27), + pos=(x + 664 * sf, s.height-90), + disabled=s.ji <= 0.5, + call=Call(s.mv, 'ji', -1) + ) + B( + '', + size=(2, s.height-17), + pos=(x + 722 * sf, 10), + style='bright' + ) + t = getattr(s.h, 'name', 'Not in a server') + a = getattr(s.h, 'address', '127.0.0.1') + p = getattr(s.h, 'port', '43210') + w = GSW(t) + B( + t if t.strip() else 'Loading...', + size=(300 * sf, 27), + pos=(x + 732 * sf, s.height-34), + disabled=not s.h, + label_scale=1 if w < 290 * sf else (290 * sf)/w, + call=Call(push, f"{t}\nHosted on build {getattr(s.h, 'build_number', '0')}" if t.strip( + ) else 'Server is still loading...\nIf it remains stuck on this\nthen either party is full, or a network issue.'), + ) + w = GSW(a) + B( + a, + size=(200 * sf, 27), + pos=(x + 732 * sf, s.height-62), + call=Call(COPY, a), + disabled=not s.h, + label_scale=1 if w < 190 * sf else (190 * sf)/w + ) + w = GSW(str(p)) + B( + str(p), + size=(97 * sf, 27), + pos=(x + 935 * sf, s.height-62), + disabled=not s.h, + call=Call(COPY, str(p)), + label_scale=1 if w < 90 * sf else (90 * sf)/w + ) + B( + 'Leave', + size=(100 * sf, 27), + pos=(x + 732 * sf, s.height-90), + call=LEAVE, + disabled=not s.h + ) + B( + 'Rejoin', + size=(97 * sf, 27), + pos=(x + 835 * sf, s.height-90), + call=Call(REJOIN, a, p, lambda: s.re), + disabled=not s.h + ) + B( + 'JRejoin', + size=(97 * sf, 27), + pos=(x + 935 * sf, s.height-90), + call=Call(s.job, Call(REJOIN, a, p, lambda: s.re), ['JRejoin', a, str(p)]), + disabled=not s.h + ) + B( + '+', + size=(50 * sf, 27), + pos=(x + 1035 * sf, s.height-34), + call=Call(s.mv, 're', 1) + ) + B( + str(s.re or 0.1), + size=(50 * sf, 27), + pos=(x + 1035 * sf, s.height-62), + call=Call( + push, f"Rejoins after {s.re or 0.1} second{['', 's'][s.re != 1]}\nKeep this 0.1 unless server kicks fast rejoins\nLife in server = job time - rejoin time") + ) + B( + '-', + size=(50 * sf, 27), + pos=(x + 1035 * sf, s.height-90), + disabled=s.re <= 0.5, + call=Call(s.mv, 're', -1) + ) + B( + '', + size=(2, s.height-17), + pos=(x + 1092 * sf, 10), + style='bright' + ) + for i, e in enumerate(s.hi.items()): + if i < s.ii: + continue + if i >= (s.ii+3): + break + g, v = e + _, a = g + n, p = v + w = GSW(n) + B( + n, + size=(300 * sf, 27), + pos=(x + 1134 * sf, s.height-34-28*(i-s.ii)), + label_scale=1 if w < 290 * sf else (290 * sf)/w, + call=Call(JOIN, a, p, False), + disabled=n == '...' + ) + nt = "Your server join history\nwill appear here. Hi." + w = GSW(nt) + 0 if len(s.hi) else T( + nt, + pos=(x + 1285 * sf, s.height-17), + h_align='center', + v_align='top', + scale=1 if w < (280*sf) else (280*sf)/w + ) + B( + cs(sc.DOWN_ARROW), + pos=(x + 1102 * sf, 10), + size=(30 * sf, s.height-17), + disabled=s.ii >= len(s.hi)-3, + call=Call(s.mv, 'ii', 1) + ) + B( + cs(sc.UP_ARROW), + pos=(x + 1436 * sf, 10), + size=(30 * sf, s.height-17), + disabled=s.ii <= 0, + call=Call(s.mv, 'ii', -1) + ) + B( + 'Force leave', + call=FORCE, + pos=(x + 1469 * sf, s.height-34), + size=(130 * sf, 27), + label_scale=0.9 + ) + B( + 'Laugh', + call=Call(chat, 'hahaha'), + pos=(x + 1469 * sf, s.height-62), + size=(130 * sf, 27) + ) + B( + 'Power', + call=Call(push, 'Power v2.5 MinUI\nExpand dev console to switch to FullUI. thanks.'), + pos=(x + 1469 * sf, s.height-90), + size=(130 * sf, 27) + ) + + def log(s, t): + s.ls.append((t, NOW())) + if s.lii < 99: + s.lii += 1 + if s.li == s.lii-17: + s.li += 1 + else: + s.ls.pop(0) + s.rf() + + def mv(s, a, i): + setattr(s, a, getattr(s, a)+i) + s.rf() + + def job(s, f, j): + s.j = j + s.lf = f + s.hd = j[1] if s.j[0] == 'JRejoin' else j[2] + if f is not None: + s._job(f) + push('Job started', color=(1, 1, 0)) + else: + push('Job stopped', color=(1, 1, 0)) + s.rf() + + def _job(s, f): + if f != s.lf: + return + s.log(f'[{s.lii:02}] [{s.j[0]}] {s.hd}') + f() + teck(s.ji or 0.1, Call(s._job, f)) + + def prv(s, c, p, n): + s.c, s.p, s.n = c, p, n + s.rf() + + def chk(s, pn): + y = 0 + for n, g in s.rr.items(): + c, p = g + if n == pn: + y = 1 + else: + for _ in p: + if pn in [_['name'], _['name_full']]: + y = 1 + if y: + s.prv(c, p, n) + break + + +HAS = app.ui_v1.has_main_window +SAVE = app.classic.save_ui_state +def KICK(f): return DISC(f()) + + +def FORCE(): return teck(0.7 if HAS() else 0.1, lambda: 0 if HAS() + else app.classic.return_to_main_menu_session_gracefully()) + + +JOIN = lambda *a: (SAVE() or 1) and CON(*a) +def GSW(s): return sw(s, suppress_warning=True) + + +def REJOIN(a, p, f): return ((LEAVE() if getattr(HOST(), 'name', '') else 0) + or 1) and teck(f() or 0.1, Call(JOIN, a, p, False)) +def COPY(s): return ((CST(s) or 1) if CIS() else push( + 'Clipboard not supported!')) and push('Copied!', color=(0, 1, 0)) + + +def NOW(): return DT.now().strftime("%H:%M:%S") + +# brobord collide grass +# ba_meta require api 9 +# ba_meta export babase.Plugin + + +class byBordd(Plugin): + def __init__(s): + C = Power + N = C.__name__ + E = ENT(N, C) + I = app.devconsole + I.tabs = [_ for _ in I.tabs if _.name != N]+[E] + I._tab_instances[N] = E.factory() diff --git a/plugins/utilities/powerup_manager.py b/plugins/utilities/powerup_manager.py new file mode 100644 index 000000000..2a81577e2 --- /dev/null +++ b/plugins/utilities/powerup_manager.py @@ -0,0 +1,2985 @@ +# ba_meta require api 9 +from __future__ import annotations + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor import bomb +from bascenev1lib.actor import powerupbox as pupbox +from bascenev1lib.actor.spazbot import SpazBot +from bascenev1lib.actor.bomb import Bomb, Blast +from bauiv1lib.popup import PopupWindow, PopupMenuWindow, PopupMenu +from bascenev1lib.actor.spaz import ( + Spaz, + SpazFactory, + PickupMessage, + PunchHitMessage, + CurseExplodeMessage, + BombDiedMessage, +) +from bascenev1lib.mainmenu import MainMenuActivity, MainMenuSession +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.actor.popuptext import PopupText +from bauiv1lib.confirm import ConfirmWindow +from bascenev1lib.actor.spaz import * + +from typing import TYPE_CHECKING + +plugman = dict( + plugin_name="powerup_manager", + description="This plugin add new modded powerups and features to manage them", + external_url="", + authors=[ + {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, + ], + version="1.0.0", +) + + +_sp_ = '\n' + +if TYPE_CHECKING: + pass + + +# === Mod updated by ATD and Less === + + +def getlanguage(text, subs: str = None, almacen: list = []): + if almacen == []: + almacen = list(range(1000)) + lang = bs.app.lang.language + translate = { + "Reset": {"Spanish": "Reiniciar", "English": "Reset", "Portuguese": "Reiniciar"}, + "Nothing": { + "Spanish": "Sin potenciadores", + "English": "No powerups", + "Portuguese": "Sem powerups", + }, + "Action 1": {"Spanish": "Potenciadores", "English": "Powerups", "Portuguese": "Powerups"}, + "Action 2": {"Spanish": "Configuración", "English": "Settings", "Portuguese": "Definições"}, + "Action 3": {"Spanish": "Extras", "English": "Extras", "Portuguese": "Extras"}, + "Action 4": {"Spanish": "Tienda", "English": "Store", "Portuguese": "Loja"}, + "Action 5": { + "Spanish": "Canjear código", + "English": "Enter Code", + "Portuguese": "Código promocional", + }, + "Custom": {"Spanish": "", "English": "Customize", "Portuguese": "Customizar"}, + "Impairment Bombs": { + "Spanish": "Bombas menoscabo", + "English": "Hyperactive bombs", + "Portuguese": "Bombas hiperativas", + }, + "Speed": {"Spanish": "Velocidad", "English": "Speed", "Portuguese": "Velocidade"}, + "Fire Bombs": { + "Spanish": "Bombas de fuego", + "English": "Fire Bombs", + "Portuguese": "Bombas de fogo", + }, + "Ice Man": { + "Spanish": "Hombre de hielo", + "English": "Ice man", + "Portuguese": "Homem de gelo", + }, + "Fly Bombs": { + "Spanish": "Bombas expansivas", + "English": "Expansive bombs", + "Portuguese": "Bombas expansivas", + }, + "Goodbye": {"Spanish": "¡Hasta luego!", "English": "Goodbye!", "Portuguese": "Adeus!"}, + "Healing Damage": { + "Spanish": "Auto-curación", + "English": "Healing Damage", + "Portuguese": "Auto-cura", + }, + "Tank Shield": { + "Spanish": "Súper blindaje", + "English": "Reinforced shield", + "Portuguese": "Escudo reforçado", + }, + "Tank Shield PTG": { + "Spanish": "Porcentaje de disminución", + "English": "Percentage decreased", + "Portuguese": "Percentual reduzido", + }, + "Healing Damage PTG": { + "Spanish": "Porcentaje de recuperación de salud", + "English": "Percentage of health recovered", + "Portuguese": "Porcentagem de recuperação de saúde", + }, + "SY: BALL": {"Spanish": "Esfera", "English": "Sphere", "Portuguese": "Esfera"}, + "SY: Impact": {"Spanish": "Especial", "English": "Special", "Portuguese": "Especial"}, + "SY: Egg": {"Spanish": "Huevito", "English": "Egg shape", "Portuguese": "Ovo"}, + "Powerup Scale": { + "Spanish": "Tamaño del potenciador", + "English": "Powerups size", + "Portuguese": "Tamanho de powerups", + }, + "Powerup With Shield": { + "Spanish": "Potenciadores con escudo", + "English": "Powerups with shield", + "Portuguese": "Powerups com escudo", + }, + "Powerup Time": { + "Spanish": "Mostrar Temporizador", + "English": "Show end time", + "Portuguese": "Mostrar cronômetro", + }, + "Powerup Style": { + "Spanish": "Forma de los potenciadores", + "English": "Shape of powerup", + "Portuguese": "Forma de powerup", + }, + "Powerup Name": { + "Spanish": "Mostrar nombre en los potenciadores", + "English": "Show name on powerups", + "Portuguese": "Mostrar nome em powerups", + }, + "Percentage": { + "Spanish": "Probabilidad", + "English": "Show percentage", + "Portuguese": "Mostrar porcentagem", + }, + "Only Items": { + "Spanish": "Sólo Accesorios", + "English": "Only utensils", + "Portuguese": "Apenas utensilios", + }, + "New": {"Spanish": "Nuevo", "English": "New", "Portuguese": "Novo"}, + "Only Bombs": { + "Spanish": "Sólo Bombas", + "English": "Only bombs", + "Portuguese": "Apenas bombas", + }, + "Coins 0": { + "Spanish": "Monedas Insuficientes", + "English": "Insufficient coins", + "Portuguese": "Moedas insuficientes", + }, + "Purchase": { + "Spanish": "Compra realizada correctamente", + "English": "Successful purchase", + "Portuguese": "Compra Bem Sucedida", + }, + "Double Product": { + "Spanish": "Ya has comprado este artículo", + "English": "You've already bought this", + "Portuguese": "Voce ja comprou isto", + }, + "Bought": {"Spanish": "Comprado", "English": "Bought", "Portuguese": "Comprou"}, + "Confirm Purchase": { + "Spanish": f'Tienes {subs} monedas. {_sp_} ¿Deseas comprar esto?', + "English": f'You have {subs} coins. {_sp_} Do you want to buy this?', + "Portuguese": f'Você tem {subs} moedas. {_sp_} Deseja comprar isto?', + }, + "FireBombs Store": { + "Spanish": 'Bombas de fuego', + "English": 'Fire bombs', + "Portuguese": 'Bombas de incêndio', + }, + "Timer Store": {"Spanish": 'Temporizador', "English": 'Timer', "Portuguese": 'Timer'}, + "Percentages Store": {"Spanish": 'Extras', "English": 'Extras', "Portuguese": 'Extras'}, + "Block Option Store": { + "Spanish": f"Uuups..{_sp_}Esta opción está bloqueada.{_sp_} Para acceder a ella puedes {_sp_} comprarla en la tienda.{_sp_} Gracias...", + "English": f"Oooops...{_sp_}This option is blocked. {_sp_} To access it you can buy {_sp_} it in the store.{_sp_} Thank you...", + "Portuguese": f"Ooops...{_sp_}Esta opção está bloqueada. {_sp_} Para acessá-lo, você pode {_sp_} comprá-lo na loja.{_sp_} Obrigado...", + }, + "True Code": { + "Spanish": "¡Código canjeado!", + "English": "Successful code!", + "Portuguese": "¡Código válido!", + }, + "False Code": { + "Spanish": "Código ya canjeado", + "English": "Expired code", + "Portuguese": "Código expirado", + }, + "Invalid Code": { + "Spanish": "Código inválido", + "English": "Invalid code", + "Portuguese": "Código inválido", + }, + "Reward Code": { + "Spanish": f"¡Felicitaciones! ¡Ganaste {subs} monedas!", + "English": f"Congratulations! You've {subs} coins", + "Portuguese": f"Parabéns! Você tem {subs} moedas", + }, + "Creator": { + "Spanish": "Mod edited by ATD", + "English": "Mod edited by ATD", + "Portuguese": "Mod edited by ATD", + }, + "Mod Info": { + "Spanish": f"Un mod genial que te permite gestionar {_sp_} los potenciadores a tu antojo. {_sp_} también incluye 8 potenciadores extra{_sp_} dejando 17 en total... ¡Guay!", + "English": f"A cool mod that allows you to manage {_sp_} powerups at your whims. {_sp_} also includes 8 extra powerups{_sp_} leaving 17 in total... Wow!", + "Portuguese": f"Um mod legal que permite que você gerencie os{_sp_} powerups de de acordo com seus caprichos. {_sp_} também inclui 8 powerups extras,{_sp_} deixando 17 no total... Uau!", + }, + "Coins Message": { + "Spanish": f"Recompensa: {subs} Monedas", + "English": f"Reward: {subs} Coins", + "Portuguese": f"Recompensa: {subs} Moedas", + }, + "Coins Limit Message": { + "Spanish": f"Ganaste {almacen[0]} Monedas.{_sp_} Pero has superado el límite de {almacen[1]}", + "English": f"You won {almacen[0]} Coins. {_sp_} But you have exceeded the limit of {almacen[1]}", + "Portuguese": f"Você ganhou {almacen[0]} Moedas. {_sp_} Mas você excedeu o limite de {almacen[1]}", + }, + } + languages = ['Spanish', 'Portuguese', 'English'] + if lang not in languages: + lang = 'English' + + if text not in translate: + return text + + return translate[text][lang] + + +def settings_distribution(): + return { + "Powers Gravity": False, + "Tank Shield PTG": 96, + "Healing Damage PTG": 72, + "Powerup Style": 'Auto', + "Powerup Scale": 1.0, + "Powerup Name": False, + "Powerup With Shield": False, + "Powerup Time": False, + } + + +apg = babase.app.config +if "PPM Settings" in apg: + old = apg['PPM Settings'] + for settings in settings_distribution(): + if settings not in old: + apg['PPM Settings'] = settings_distribution() +else: + apg['PPM Settings'] = settings_distribution() +apg.apply_and_commit() + +config = apg['PPM Settings'] + + +def default_powerups(): + return { + "Shield": 2, + "Punch": 3, + "Mine Bombs": 2, + "Impact Bombs": 3, + "Ice Bombs": 3, + "Triple": 3, + "Sticky Bombs": 3, + "Curse": 1, + "Health": 1, + "Speed": 2, + "Healing Damage": 1, + "Goodbye": 2, + "Ice Man": 1, + "Tank Shield": 1, + "Impairment Bombs": 2, + "Fire Bombs": 3, + "Fly Bombs": 3, + } + + +if "Powerups" in config: + p_old = config['Powerups'] + for powerups in default_powerups(): + if powerups not in p_old: + config['Powerups'] = default_powerups() +else: + config['Powerups'] = default_powerups() +apg.apply_and_commit() + +powerups = config['Powerups'] + +# === EXTRAS === + +GLOBAL = {"Tab": 'Action 1', "Cls Powerup": 0, "Coins Message": []} + + +# === STORE === +def promo_codes(): + return { + "G-Am54igO42Os": [True, 1100], + "P-tRo8nM8dZ": [True, 2800], + "Y-tU2B3S": [True, 500], + "B-0mB3RYT2z": [True, 910], + "B-Asd14mON9G0D": [True, 910], + "D-rAcK0cJ23": [True, 910], + "E-a27ZO6f3Y": [True, 600], + "E-Am54igO42Os": [True, 600], + "E-M4uN3K34XB": [True, 840], + "PM-731ClcAF": [True, 50000], + } + + +def store_items(): + return {"Buy Firebombs": True, "Buy Option": True, "Buy Percentage": True} + + +if apg.get('Bear Coin') is None: + apg['Bear Coin'] = 0 + apg.apply_and_commit() + +if apg.get('Bear Coin') is not None: + if apg['Bear Coin'] <= 0: + apg['Bear Coin'] = 0 + apg['Bear Coin'] = int(apg['Bear Coin']) + +if apg.get('Bear Store') is None: + apg['Bear Store'] = {} + +for i, j in store_items().items(): + store = apg['Bear Store'] + if i not in store: + if store.get(i) is None: + store[i] = j + apg.apply_and_commit() + +STORE = apg['Bear Store'] + +if STORE.get('Promo Code') is None: + STORE['Promo Code'] = promo_codes() + +for i, x in promo_codes().items(): + pmcode = STORE['Promo Code'] + if i not in pmcode: + if pmcode.get(i) is None: + pmcode[i] = x + +apg.apply_and_commit() + + +class BearStore: + def __init__(self, price: int = 1000, value: str = '', callback: call = None): + + self.price = price + self.value = value + self.store = STORE[value] + self.coins = apg['Bear Coin'] + self.callback = callback + + def buy(self): + if not self.store: + if self.coins >= (self.price): + + def confirm(): + STORE[self.value] = True + apg['Bear Coin'] -= int(self.price) + bs.broadcastmessage(getlanguage('Purchase'), (0, 1, 0)) + bui.getsound('cashRegister').play() + apg.apply_and_commit() + self.callback() + + ConfirmWindow( + getlanguage('Confirm Purchase', subs=self.coins), + width=400, + height=120, + action=confirm, + ok_text=babase.Lstr(resource='okText'), + ) + else: + bs.broadcastmessage(getlanguage('Coins 0'), (1, 0, 0)) + bui.getsound('error').play() + else: + bs.broadcastmessage(getlanguage('Double Product'), (1, 0, 0)) + bui.getsound('error').play() + + def __del__(self): + apg['Bear Coin'] = int(apg['Bear Coin']) + apg.apply_and_commit() + + +class PromoCode: + def __init__(self, code: str = ''): + self.code = code + self.codes_store = STORE['Promo Code'] + if self.code in self.codes_store: + self.code_type = STORE['Promo Code'][code] + self.promo_code_expire = self.code_type[0] + self.promo_code_amount = self.code_type[1] + + def __del__(self): + apg['Bear Coin'] = int(apg['Bear Coin']) + apg.apply_and_commit() + + def code_confirmation(self): + if self.code != "": + bs.broadcastmessage(babase.Lstr(resource='submittingPromoCodeText'), (0, 1, 0)) + try: + babase.pushcall(babase.CallPartial(self.validate_code), from_other_thread=True) + except: + pass + + def validate_code(self): + if self.code in self.codes_store: + if self.promo_code_expire: + with babase.ContextRef.empty(): + babase.pushcall( + babase.CallPartial(self.successful_code), from_other_thread=True + ) + bs.broadcastmessage(getlanguage('True Code'), (0, 1, 0)) + bui.getsound('cheer').play() + self.code_type[0] = False + else: + bs.broadcastmessage(getlanguage('False Code'), (1, 0, 0)) + bui.getsound('error').play() + else: + bs.broadcastmessage(getlanguage('Invalid Code'), (1, 0, 0)) + bui.getsound('error').play() + + def successful_code(self): + apg['Bear Coin'] += self.promo_code_amount + bs.broadcastmessage(getlanguage('Reward Code', subs=self.promo_code_amount), (0, 1, 0)) + bui.getsound('cashRegister2').play() + + +MainMenuActivity.super_transition_in = MainMenuActivity.on_transition_in + + +def new_on_transition_in(self): + self.super_transition_in() + limit = 8400 + bear_coin = apg['Bear Coin'] + coins_message = GLOBAL['Coins Message'] + try: + if not (STORE['Buy Firebombs'] and STORE['Buy Option'] and STORE['Buy Percentage']): + + if coins_message != []: + result = 0 + for i in coins_message: + result += i + + if not bear_coin >= (limit - 1): + bs.broadcastmessage(getlanguage('Coins Message', subs=result), (0, 1, 0)) + bui.getsound('cashRegister').play() + else: + bs.broadcastmessage( + getlanguage('Coins Limit Message', almacen=[result, limit]), (1, 0, 0) + ) + bui.getsound('error').play() + self.bear_coin_message = True + GLOBAL['Coins Message'] = [] + except: + pass + + +SpazBot.super_handlemessage = SpazBot.handlemessage + + +def bot_handlemessage(self, msg: Any): + self.super_handlemessage(msg) + if isinstance(msg, bs.DieMessage): + if not self.die: + self.die = True + self.limit = 8400 + self.free_coins = random.randint(1, 25) + self.bear_coins = apg['Bear Coin'] + + if not self.bear_coins >= (self.limit): + self.bear_coins += self.free_coins + GLOBAL['Coins Message'].append(self.free_coins) + + if self.bear_coins >= (self.limit): + self.bear_coins = self.limit + + apg['Bear Coin'] = int(self.bear_coins) + apg.apply_and_commit() + + else: + GLOBAL['Coins Message'].append(self.free_coins) + + +def cls_pow_color(): + return [ + (1, 0.1, 0.1), + (0.1, 0.5, 0.9), + (0.1, 0.9, 0.9), + (0.1, 0.9, 0.1), + (0.1, 1, 0.5), + (1, 1, 0.2), + (2, 0.5, 0.5), + (1, 0, 6), + ] + + +def random_color(): + a = random.random() * 3 + b = random.random() * 3 + c = random.random() * 3 + return (a, b, c) + + +def powerup_dist(): + return ( + ('triple_bombs', powerups['Triple']), + ('ice_bombs', powerups['Ice Bombs']), + ('punch', powerups['Punch']), + ('impact_bombs', powerups['Impact Bombs']), + ('land_mines', powerups['Mine Bombs']), + ('sticky_bombs', powerups['Sticky Bombs']), + ('shield', powerups['Shield']), + ('health', powerups['Health']), + ('curse', powerups['Curse']), + ('speed', powerups['Speed']), + ('health_damage', powerups['Healing Damage']), + ('goodbye', powerups['Goodbye']), + ('ice_man', powerups['Ice Man']), + ('tank_shield', powerups['Tank Shield']), + ('impairment_bombs', powerups['Impairment Bombs']), + ('fire_bombs', powerups['Fire Bombs']), + ('fly_bombs', powerups['Fly Bombs']), + ) + + +def percentage_tank_shield(): + percentage = config['Tank Shield PTG'] + percentage_text = ('0.') + str(percentage) + return float(percentage_text) + + +def percentage_health_damage(): + percentage = config['Healing Damage PTG'] + percentage_text = ('0.') + str(percentage) + return float(percentage_text) + + +# === Modify class === + + +class NewProfileBrowserWindow: + def __init__( + self, + transition: str = 'in_right', + in_main_menu: bool = True, + selected_profile: str = None, + origin_widget: bui.Widget = None, + ): + super().__init__(transition, in_main_menu, selected_profile, origin_widget) + + self.session = bs.get_foreground_host_session() + uiscale = bui.app.ui_v1.uiscale + width = 100 if uiscale is babase.UIScale.SMALL else -14 + size = 50 + position = (width * 1.65, 300) + + if isinstance(self.session, MainMenuSession): + self.button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=position, + size=(size, size), + button_type='square', + label='', + on_activate_call=babase.CallPartial(self.powerupmanager_window), + ) + + size = size * 0.60 + self.image = bui.imagewidget( + parent=self._root_widget, + size=(size, size), + draw_controller=self.button, + position=(position[0] + 10.5, position[1] + 17), + texture=bui.gettexture('powerupSpeed'), + ) + + self.text = bui.textwidget( + parent=self._root_widget, + position=(position[0] + 25, position[1] + 10), + size=(0, 0), + scale=0.45, + color=(0.7, 0.9, 0.7, 1.0), + draw_controller=self.button, + maxwidth=60, + text=(f"Ultimate Powerup {_sp_}Manager"), + h_align='center', + v_align='center', + ) + + def powerupmanager_window(self): + bui.containerwidget(edit=self._root_widget, transition='out_left') + PowerupManagerWindow() + + +class NewPowerupBoxFactory(pupbox.PowerupBoxFactory): + def __init__(self) -> None: + super().__init__() + self.tex_speed = bs.gettexture('powerupSpeed') + self.tex_health_damage = bs.gettexture('heart') + self.tex_goodbye = bs.gettexture('achievementOnslaught') + self.tex_ice_man = bs.gettexture('ouyaUButton') + self.tex_tank_shield = bs.gettexture('achievementSuperPunch') + self.tex_impairment_bombs = bs.gettexture('levelIcon') + self.tex_fire_bombs = bs.gettexture('ouyaOButton') + self.tex_fly_bombs = bs.gettexture('star') + + self._powerupdist = [] + for powerup, freq in powerup_dist(): + for _i in range(int(freq)): + self._powerupdist.append(powerup) + + def get_random_powerup_type(self, forcetype=None, excludetypes=None): + + try: + self.mapa = bs.getactivity()._map.getname() + except: + self.mapa = None + + speed_banned_maps = ['Hockey Stadium', 'Lake Frigid', 'Happy Thoughts'] + + if self.mapa in speed_banned_maps: + powerup_disable = ['speed'] + else: + powerup_disable = [] + + if excludetypes is None: + excludetypes = [] + if forcetype: + ptype = forcetype + else: + if self._lastpoweruptype == 'curse': + ptype = 'health' + else: + while True: + ptype = self._powerupdist[random.randint(0, len(self._powerupdist) - 1)] + if ptype not in excludetypes and ptype not in powerup_disable: + break + self._lastpoweruptype = ptype + return ptype + + +def fire_effect(self): + if self.node.exists(): + bs.emitfx( + position=self.node.position, scale=3, count=50 * 2, spread=0.3, chunk_type='sweat' + ) + else: + self.fire_effect_time = None + + +# BOMBS +Bomb._pm_old_bomb = Bomb.__init__ + + +def _bomb_init( + self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + bomb_type: str = 'normal', + blast_radius: float = 2.0, + bomb_scale: float = 1.0, + source_player: bs.Player = None, + owner: bs.Node = None, +): + + self.bm_type = bomb_type + new_bomb_type = 'ice' if bomb_type in ['ice_bubble', 'impairment', 'fire', 'fly'] else bomb_type + + # Call original __init__ + self._pm_old_bomb( + position=position, + velocity=velocity, + bomb_type=new_bomb_type, + blast_radius=blast_radius, + bomb_scale=bomb_scale, + source_player=source_player, + owner=owner, + ) + + tex = self.node.color_texture + + if self.bm_type == 'ice_bubble': + self.bomb_type = self.bm_type + self.node.mesh = None + self.shield_ice = bs.newnode( + 'shield', owner=self.node, attrs={'color': (0.5, 1.0, 7.0), 'radius': 0.6} + ) + self.node.connectattr('position', self.shield_ice, 'position') + + elif self.bm_type == 'fire': + self.bomb_type = self.bm_type + self.node.mesh = None + self.shield_fire = bs.newnode( + 'shield', owner=self.node, attrs={'color': (6.5, 6.5, 2.0), 'radius': 0.6} + ) + self.node.connectattr('position', self.shield_fire, 'position') + self.fire_effect_time = bs.Timer(0.1, bs.WeakCallPartial(fire_effect, self), repeat=True) + elif self.bm_type == 'impairment': + self.bomb_type = self.bm_type + tex = bs.gettexture('eggTex3') + elif self.bm_type == 'fly': + self.bomb_type = self.bm_type + tex = bs.gettexture('eggTex1') + + if 'tex' in locals(): + self.node.color_texture = tex + self.hit_subtype = self.bomb_type + + if self.bomb_type == 'ice_bubble': + self.blast_radius *= 1.2 + elif self.bomb_type == 'fly': + self.blast_radius *= 2.2 + + +def bomb_handlemessage(self, msg: Any) -> Any: + assert not self.expired + + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, bomb.ExplodeHitMessage): + node = bs.getcollision().opposingnode + assert self.node + nodepos = self.node.position + mag = 2000.0 + if self.blast_type in ('ice', 'ice_bubble'): + mag *= 0.5 + elif self.blast_type == 'land_mine': + mag *= 2.5 + elif self.blast_type == 'tnt': + mag *= 2.0 + elif self.blast_type == 'fire': + mag *= 0.6 + elif self.blast_type == 'fly': + mag *= 5.5 + + node.handlemessage( + bs.HitMessage( + pos=nodepos, + velocity=(0, 0, 0), + magnitude=mag, + hit_type=self.hit_type, + hit_subtype=self.hit_subtype, + radius=self.radius, + source_player=babase.existing(self._source_player), + ) + ) + if self.blast_type in ('ice', 'ice_bubble'): + bomb.BombFactory.get().freeze_sound.play(10) + node.handlemessage(bs.FreezeMessage()) + + return None + + +def powerup_translated(self, type: str): + powerups_names = { + 'triple_bombs': babase.Lstr(resource='helpWindow.' + 'powerupBombNameText'), + 'ice_bombs': babase.Lstr(resource='helpWindow.' + 'powerupIceBombsNameText'), + 'punch': babase.Lstr(resource='helpWindow.' + 'powerupPunchNameText'), + 'impact_bombs': babase.Lstr(resource='helpWindow.' + 'powerupImpactBombsNameText'), + 'land_mines': babase.Lstr(resource='helpWindow.' + 'powerupLandMinesNameText'), + 'sticky_bombs': babase.Lstr(resource='helpWindow.' + 'powerupStickyBombsNameText'), + 'shield': babase.Lstr(resource='helpWindow.' + 'powerupShieldNameText'), + 'health': babase.Lstr(resource='helpWindow.' + 'powerupHealthNameText'), + 'curse': babase.Lstr(resource='helpWindow.' + 'powerupCurseNameText'), + 'speed': getlanguage('Speed'), + 'health_damage': getlanguage('Healing Damage'), + 'goodbye': getlanguage('Goodbye'), + 'ice_man': getlanguage('Ice Man'), + 'tank_shield': getlanguage('Tank Shield'), + 'impairment_bombs': getlanguage('Impairment Bombs'), + 'fire_bombs': getlanguage('Fire Bombs'), + 'fly_bombs': getlanguage('Fly Bombs'), + } + self.texts['Name'].text = powerups_names[type] + + +# POWERUP +pupbox.PowerupBox._old_pbx_ = pupbox.PowerupBox.__init__ + + +def _pbx_( + self, + position: Sequence[float] = (0.0, 1.0, 0.0), + poweruptype: str = 'triple_bombs', + expire: bool = True, +): + self.news: list = [] + for x, i in powerup_dist(): + self.news.append(x) + + self.box: list = [] + self.texts = {} + self.news = self.news[9:] + self.box.append(poweruptype) + self.npowerup = self.box[0] + factory = NewPowerupBoxFactory.get() + + if self.npowerup in self.news: + new_poweruptype = 'shield' + else: + new_poweruptype = poweruptype + self._old_pbx_(position, new_poweruptype, expire) + + type = new_poweruptype + tex = self.node.color_texture + mesh = self.node.mesh + + if self.npowerup == 'speed': + type = self.npowerup + tex = factory.tex_speed + elif self.npowerup == 'health_damage': + type = self.npowerup + tex = factory.tex_health_damage + elif self.npowerup == 'goodbye': + type = self.npowerup + tex = factory.tex_goodbye + elif self.npowerup == 'ice_man': + type = self.npowerup + tex = factory.tex_ice_man + elif self.npowerup == 'tank_shield': + type = self.npowerup + tex = factory.tex_tank_shield + elif self.npowerup == 'impairment_bombs': + type = self.npowerup + tex = factory.tex_impairment_bombs + elif self.npowerup == 'fire_bombs': + type = self.npowerup + tex = factory.tex_fire_bombs + elif self.npowerup == 'fly_bombs': + type = self.npowerup + tex = factory.tex_fly_bombs + + self.poweruptype = type + self.node.mesh = mesh + self.node.color_texture = tex + n_scale = config['Powerup Scale'] + style = config['Powerup Style'] + + curve = bs.animate(self.node, 'mesh_scale', {0: 0, 0.14: 1.6, 0.2: n_scale}) + bs.timer(0.2, curve.delete) + + def util_text( + type: str, + text: str, + scale: float = 1, + color: list = [1, 1, 1], + position: list = [0, 0.7, 0], + colors_name: bool = False, + ): + m = bs.newnode( + 'math', + owner=self.node, + attrs={'input1': (position[0], position[1], position[2]), 'operation': 'add'}, + ) + self.node.connectattr('position', m, 'input2') + self.texts[type] = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': str(text), + 'in_world': True, + 'scale': 0.02, + 'shadow': 0.5, + 'flatness': 1.0, + 'color': (color[0], color[1], color[2]), + 'h_align': 'center', + }, + ) + m.connectattr('output', self.texts[type], 'position') + bs.animate(self.texts[type], 'scale', {0: 0.017, 0.4: 0.017, 0.5: 0.01 * scale}) + + if colors_name: + bs.animate_array( + self.texts[type], + 'color', + 3, + { + 0: (1, 0, 0), + 0.2: (1, 0.5, 0), + 0.4: (1, 1, 0), + 0.6: (0, 1, 0), + 0.8: (0, 1, 1), + 1.0: (1, 0, 1), + 1.2: (1, 0, 0), + }, + loop=True, + ) + + def update_time(time): + if hasattr(self, 'texts') and 'Time' in self.texts and self.texts['Time']: + self.texts['Time'].text = str(time) + + if config['Powerup Time']: + interval = int(pupbox.DEFAULT_POWERUP_INTERVAL) + time2 = interval - 1 + time = 1 + + util_text( + 'Time', time2, scale=1.5, color=(2, 2, 2), position=[0, 0.9, 0], colors_name=False + ) + + while interval + 3: + bs.timer(time - 1, babase.CallPartial(update_time, f'{time2}s')) + + if time2 == 0: + break + + time += 1 + time2 -= 1 + + if config['Powerup With Shield']: + scale = config['Powerup Scale'] + self.shield = bs.newnode( + 'shield', owner=self.node, attrs={'color': (1, 1, 0), 'radius': 1.3 * scale} + ) + self.node.connectattr('position', self.shield, 'position') + bs.animate_array( + self.shield, + 'color', + 3, + {0: (2, 0, 0), 0.5: (0, 2, 0), 1: (0, 1, 6), 1.5: (2, 0, 0)}, + loop=True, + ) + + if config['Powerup Name']: + util_text('Name', self.poweruptype, scale=1.2, position=[0, 0.4, 0], colors_name=True) + powerup_translated(self, self.poweruptype) + + if style == 'SY: BALL': + self.node.mesh = bs.getmesh('frostyPelvis') + elif style == 'SY: Impact': + self.node.mesh = bs.getmesh('impactBomb') + elif style == 'SY: Egg': + self.node.mesh = bs.getmesh('egg') + + +# SPAZ +def _speed_off_flash(self): + if self.node: + factory = NewPowerupBoxFactory.get() + self.node.billboard_texture = factory.tex_speed + self.node.billboard_opacity = 1.0 + self.node.billboard_cross_out = True + + +def _speed_wear_off(self): + if self.node: + self.node.hockey = False + self.node.billboard_opacity = 0.0 + bui.getsound('powerdown01').play() + + +def _ice_man_off_flash(self): + if self.node: + factory = NewPowerupBoxFactory.get() + self.node.billboard_texture = factory.tex_ice_man + self.node.billboard_opacity = 1.0 + self.node.billboard_cross_out = True + + +def _ice_man_wear_off(self): + if self.node: + f = self.color[0] + i = (0, 1, 4) + + bomb = self.bmb_color[0] + if bomb != 'ice_bubble': + self.bomb_type = bomb + else: + self.bomb_type = 'normal' + + self.freeze_punch = False + self.node.billboard_opacity = 0.0 + bs.animate_array(self.node, 'color', 3, {0: f, 0.3: i, 0.6: f}) + bui.getsound('powerdown01').play() + + +Spaz._pm2_spz_old = Spaz.__init__ + + +def _init_spaz_(self, *args, **kwargs): + self._pm2_spz_old(*args, **kwargs) + self.edg_eff = False + self.kill_eff = False + self.freeze_punch = False + self.die = False + self.color: list = [] + self.color.append(self.node.color) + + self.tankshield = {"Tank": False, "Reduction": False, "Shield": None} + + +Spaz._super_on_punch_press = Spaz.on_punch_press + + +def spaz_on_punch_press(self) -> None: + self._super_on_punch_press() + + if self.tankshield['Tank']: + try: + self.tankshield['Reduction'] = True + + shield = bs.newnode( + 'shield', owner=self.node, attrs={'color': (4, 1, 4), 'radius': 1.3} + ) + self.node.connectattr('position_center', shield, 'position') + + self.tankshield['Shield'] = shield + except: + pass + + +Spaz._super_on_punch_release = Spaz.on_punch_release + + +def spaz_on_punch_release(self) -> None: + self._super_on_punch_release() + try: + self.tankshield['Shield'].delete() + self.tankshield['Reduction'] = False + except: + pass + + +def new_get_bomb_type_tex(self) -> babase.Texture: + factory = NewPowerupBoxFactory.get() + if self.bomb_type == 'sticky': + return factory.tex_sticky_bombs + if self.bomb_type == 'ice': + return factory.tex_ice_bombs + if self.bomb_type == 'impact': + return factory.tex_impact_bombs + if self.bomb_type == 'impairment': + return factory.tex_impairment_bombs + if self.bomb_type == 'fire': + return factory.tex_fire_bombs + if self.bomb_type == 'fly': + return factory.tex_fly_bombs + return None + + +def new_handlemessage(self, msg: Any) -> Any: + assert not self.expired + + if isinstance(msg, bs.PickedUpMessage): + if self.node: + self.node.handlemessage('hurt_sound') + self.node.handlemessage('picked_up') + + self._num_times_hit += 1 + + elif isinstance(msg, bs.ShouldShatterMessage): + bs.timer(0.001, bs.WeakCallPartial(self.shatter)) + + elif isinstance(msg, bs.ImpactDamageMessage): + bs.timer(0.001, bs.WeakCallPartial(self._hit_self, msg.intensity)) + elif isinstance(msg, bs.PowerupMessage): + factory = NewPowerupBoxFactory.get() + if self._dead or not self.node: + return True + if self.pick_up_powerup_callback is not None: + self.pick_up_powerup_callback(self) + if msg.poweruptype == 'triple_bombs': + tex = PowerupBoxFactory.get().tex_bomb + self._flash_billboard(tex) + self.set_bomb_count(3) + if self.powerups_expire: + self.node.mini_billboard_1_texture = tex + t_ms = int(bs.time() * 1000) + assert isinstance(t_ms, int) + self.node.mini_billboard_1_start_time = t_ms + self.node.mini_billboard_1_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._multi_bomb_wear_off_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000), + babase.CallPartial(self._multi_bomb_wear_off_flash), + ) + self._multi_bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME, babase.CallPartial(self._multi_bomb_wear_off) + ) + elif msg.poweruptype == 'land_mines': + self.set_land_mine_count(min(self.land_mine_count + 3, 3)) + elif msg.poweruptype == 'impact_bombs': + self.bomb_type = 'impact' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = int(bs.time() * 1000) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME - 2000, babase.CallPartial(self._bomb_wear_off_flash) + ) + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME, babase.CallPartial(self._bomb_wear_off) + ) + elif msg.poweruptype == 'sticky_bombs': + self.bomb_type = 'sticky' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = int(bs.time() * 1000) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME - 2000, babase.CallPartial(self._bomb_wear_off_flash) + ) + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME, babase.CallPartial(self._bomb_wear_off) + ) + elif msg.poweruptype == 'punch': + self._has_boxing_gloves = True + tex = PowerupBoxFactory.get().tex_punch + self._flash_billboard(tex) + self.equip_boxing_gloves() + if self.powerups_expire: + self.node.boxing_gloves_flashing = False + self.node.mini_billboard_3_texture = tex + t_ms = int(bs.time() * 1000) + assert isinstance(t_ms, int) + self.node.mini_billboard_3_start_time = t_ms + self.node.mini_billboard_3_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._boxing_gloves_wear_off_flash_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME - 2000, bs.WeakCallPartial(self._gloves_wear_off_flash) + ) + self._boxing_gloves_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME, + bs.WeakCallPartial(self._gloves_wear_off), + ) + elif msg.poweruptype == 'shield': + factory = SpazFactory.get() + self.equip_shields(decay=factory.shield_decay_rate > 0) + elif msg.poweruptype == 'curse': + self.curse() + elif msg.poweruptype == 'ice_bombs': + self.bomb_type = 'ice' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCallPartial(self._bomb_wear_off_flash), + ) + + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) + ) + + elif msg.poweruptype == 'health': + if self.edg_eff: + f = self.color[0] + r = (2, 0, 0) + g = (0, 2, 0) + bs.animate_array(self.node, 'color', 3, {0: r, 0.6: g, 1.0: f}) + self.edg_eff = False + if self._cursed: + self._cursed = False + factory = SpazFactory.get() + for attr in ['materials', 'roller_materials']: + materials = getattr(self.node, attr) + if factory.curse_material in materials: + setattr( + self.node, + attr, + tuple(m for m in materials if m != factory.curse_material), + ) + self.node.curse_death_time = 0 + self.hitpoints = self.hitpoints_max + self._flash_billboard(PowerupBoxFactory.get().tex_health) + self.node.hurt = 0 + self._last_hit_time = None + self._num_times_hit = 0 + + elif msg.poweruptype == 'tank_shield': + self.tankshield['Tank'] = True + self.edg_eff = False + tex = factory.tex_tank_shield + self._flash_billboard(tex) + + elif msg.poweruptype == 'health_damage': + tex = factory.tex_health_damage + self.edg_eff = True + f = self.color[0] + i = (2, 0.5, 2) + bs.animate_array(self.node, 'color', 3, {0: i, 0.5: i, 0.6: f}) + self._flash_billboard(tex) + self.tankshield['Tank'] = False + self.freeze_punch = False + + elif msg.poweruptype == 'goodbye': + tex = factory.tex_goodbye + self._flash_billboard(tex) + self.kill_eff = True + + elif msg.poweruptype == 'fly_bombs': + self.bomb_type = 'fly' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCallStrict(self._bomb_wear_off_flash), + ) + + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) + ) + + elif msg.poweruptype == 'fire_bombs': + self.bomb_type = 'fire' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCallStrict(self._bomb_wear_off_flash), + ) + + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) + ) + + elif msg.poweruptype == 'impairment_bombs': + self.bomb_type = 'impairment' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + POWERUP_WEAR_OFF_TIME + self._bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCallStrict(self._bomb_wear_off_flash), + ) + + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, bs.WeakCallStrict(self._bomb_wear_off) + ) + + elif msg.poweruptype == 'ice_man': + tex = factory.tex_ice_man + self.bomb_type = 'ice_bubble' + self.freeze_punch = True + self.edg_eff = False + self.node.color = (0, 1, 4) + self._flash_billboard(tex) + + if self.powerups_expire: + ice_man_time = 17000 + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + ice_man_time + + self.ice_man_flash_timer = bs.Timer( + (ice_man_time - 2000) / 1000.0, babase.CallPartial(_ice_man_off_flash, self) + ) + + self.ice_man_timer = bs.Timer( + ice_man_time / 1000.0, babase.CallPartial(_ice_man_wear_off, self) + ) + + elif msg.poweruptype == 'speed': + self.node.hockey = True + tex = factory.tex_speed + self._flash_billboard(tex) + if self.powerups_expire: + speed_time = 15000 + self.node.mini_billboard_2_texture = tex + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = t_ms + speed_time + + self.speed_flash_timer = bs.Timer( + (speed_time - 2000) / 1000.0, babase.Call(_speed_off_flash, self) + ) + + self.speed_timer = bs.Timer( + speed_time / 1000.0, bs.WeakCallPartial(_speed_wear_off, self) + ) + + self.bmb_color: list = [] + self.bmb_color.append(self.bomb_type) + + self.node.handlemessage('flash') + if msg.sourcenode: + msg.sourcenode.handlemessage(bs.PowerupAcceptMessage()) + return True + + elif isinstance(msg, bs.FreezeMessage): + if not self.node: + return None + if self.node.invincible: + SpazFactory.get().block_sound.play(1.0, self.node.position) + return None + if self.shield: + return None + if not self.frozen: + self.frozen = True + self.node.frozen = True + bs.timer(5.0, bs.WeakCallPartial(self.handlemessage, bs.ThawMessage())) + if self.hitpoints <= 0: + self.shatter() + if self.freeze_punch: + self.handlemessage(bs.ThawMessage()) + + elif isinstance(msg, bs.ThawMessage): + if self.frozen and not self.shattered and self.node: + self.frozen = False + self.node.frozen = False + + elif isinstance(msg, bs.HitMessage): + if not self.node: + return None + if self.node.invincible: + SpazFactory.get().block_sound.play(1.0, self.node.position) + return True + + local_time = bs.time() * 1000 + assert isinstance(local_time, int) + if self._last_hit_time is None or local_time - self._last_hit_time > 1000: + self._num_times_hit += 1 + self._last_hit_time = local_time + + mag = msg.magnitude * self.impact_scale + velocity_mag = msg.velocity_magnitude * self.impact_scale + damage_scale = 0.22 + + def fire_effect(): + if not self.shield: + if self.node.exists(): + bs.emitfx( + position=self.node.position, + scale=3, + count=50 * 2, + spread=0.3, + chunk_type='sweat', + ) + self.node.handlemessage('celebrate', 560) + else: + self._fire_time = None + else: + self._fire_time = None + + def fire(time, damage): + if not self.shield and not self._dead: + self.hitpoints -= damage + bs.show_damage_count(f'-{damage}HP', self.node.position, msg.force_direction) + bui.getsound('fuse01').play() + + if duration != time: + self._fire_time = bs.Timer(0.1, babase.CallPartial(fire_effect), repeat=True) + else: + self._fire_time = None + + if self.hitpoints < 0: + self.node.handlemessage(bs.DieMessage()) + + if msg.hit_subtype == 'fly': + damage_scale = 0.0 + + if self.shield: + self.shield_hitpoints -= 300 + + if self.shield_hitpoints < 0: + self.shield.delete() + self.shield = None + SpazFactory.get().shield_down_sound.play(1.0, self.node.position) + elif msg.hit_subtype == 'fire': + index = 1 + duration = 5 + damage = 103 + if not self.shield: + for firex in range(duration): + bs.timer(index, bs.WeakCallPartial(fire, index, damage)) + self._fire_time = bs.Timer(0.1, babase.CallPartial(fire_effect), repeat=True) + index += 1 + else: + self.shield_hitpoints -= 80 + if self.shield_hitpoints < 1: + self.shield.delete() + self.shield = None + SpazFactory.get().shield_down_sound.play(1.0, self.node.position) + elif msg.hit_subtype == 'impairment': + damage_scale = 0 + + if self.shield: + self.shield.delete() + self.shield = None + SpazFactory.get().shield_down_sound.play(1.0, self.node.position) + else: + hitpoints = int(self.hitpoints * 0.80) + self.hitpoints -= int(hitpoints) + bs.show_damage_count( + (f'-{int(hitpoints / 10)}%'), self.node.position, msg.force_direction + ) + + if self.hitpoints < 0 or hitpoints < 95: + self.node.handlemessage(bs.DieMessage()) + + if self.shield: + if msg.flat_damage: + damage = msg.flat_damage * self.impact_scale + else: + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', + msg.pos[0], + msg.pos[1], + msg.pos[2], + msg.velocity[0], + msg.velocity[1], + msg.velocity[2], + mag, + velocity_mag, + msg.radius, + 1, + msg.force_direction[0], + msg.force_direction[1], + msg.force_direction[2], + ) + damage = damage_scale * self.node.damage + + assert self.shield_hitpoints is not None + self.shield_hitpoints -= int(damage) + self.shield.hurt = 1.0 - float(self.shield_hitpoints) / self.shield_hitpoints_max + + max_spillover = SpazFactory.get().max_shield_spillover_damage + if self.shield_hitpoints <= 0: + + self.shield.delete() + self.shield = None + SpazFactory.get().shield_down_sound.play(1.0, self.node.position) + + npos = self.node.position + bs.emitfx( + position=(npos[0], npos[1] + 0.9, npos[2]), + velocity=self.node.velocity, + count=random.randrange(20, 30), + scale=1.0, + spread=0.6, + chunk_type='spark', + ) + + else: + SpazFactory.get().shield_hit_sound.play(0.5, self.node.position) + + assert msg.force_direction is not None + bs.emitfx( + position=msg.pos, + velocity=( + msg.force_direction[0] * 1.0, + msg.force_direction[1] * 1.0, + msg.force_direction[2] * 1.0, + ), + count=min(30, 5 + int(damage * 0.005)), + scale=0.5, + spread=0.3, + chunk_type='spark', + ) + + if self.shield_hitpoints <= -max_spillover: + leftover_damage = -max_spillover - self.shield_hitpoints + shield_leftover_ratio = leftover_damage / damage + + mag *= shield_leftover_ratio + velocity_mag *= shield_leftover_ratio + else: + return True + else: + shield_leftover_ratio = 1.0 + + if msg.flat_damage: + damage = int(msg.flat_damage * self.impact_scale * shield_leftover_ratio) + else: + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', + msg.pos[0], + msg.pos[1], + msg.pos[2], + msg.velocity[0], + msg.velocity[1], + msg.velocity[2], + mag, + velocity_mag, + msg.radius, + 0, + msg.force_direction[0], + msg.force_direction[1], + msg.force_direction[2], + ) + + damage = int(damage_scale * self.node.damage) + + if self.tankshield['Reduction']: + porcentaje = percentage_tank_shield() + dism = int(damage * porcentaje) + damage = int(damage - dism) + + bs.show_damage_count('-' + str(int(damage / 10)) + '%', msg.pos, msg.force_direction) + + self.node.handlemessage('hurt_sound') + + if self.edg_eff: + porcentaje = percentage_health_damage() + dmg_dism = int(damage * porcentaje) + self.hitpoints += dmg_dism + + PopupText( + text=f'+{int(dmg_dism / 10)}%', + scale=1.5, + position=self.node.position, + color=(0, 1, 0), + ).autoretain() + bs.animate_array( + self.node, 'color', 3, {0: (0, 1, 0), 0.39: (0, 2, 0), 0.4: self.color[0]} + ) + bui.getsound('healthPowerup').play() + + if msg.hit_type == 'punch': + self.on_punched(damage) + + try: + if msg.get_source_player(bs.Player).actor.freeze_punch: + self.node.color = (0, 1, 4) + bui.getsound('freeze').play() + self.node.handlemessage(bs.FreezeMessage()) + except: + pass + + if damage > 350: + assert msg.force_direction is not None + bs.show_damage_count( + '-' + str(int(damage / 10)) + '%', msg.pos, msg.force_direction + ) + + if msg.hit_subtype == 'super_punch': + SpazFactory.get().punch_sound_stronger.play(1.0, self.node.position) + if damage > 500: + sounds = SpazFactory.get().punch_sound_strong + sound = sounds[random.randrange(len(sounds))] + else: + sound = SpazFactory.get().punch_sound + sound.play(1.0, self.node.position) + + assert msg.force_direction is not None + bs.emitfx( + position=msg.pos, + velocity=( + msg.force_direction[0] * 0.5, + msg.force_direction[1] * 0.5, + msg.force_direction[2] * 0.5, + ), + count=min(10, 1 + int(damage * 0.0025)), + scale=0.3, + spread=0.03, + ) + + bs.emitfx( + position=msg.pos, + chunk_type='sweat', + velocity=( + msg.force_direction[0] * 1.3, + msg.force_direction[1] * 1.3 + 5.0, + msg.force_direction[2] * 1.3, + ), + count=min(30, 1 + int(damage * 0.04)), + scale=0.9, + spread=0.28, + ) + + hurtiness = damage * 0.003 + punchpos = ( + msg.pos[0] + msg.force_direction[0] * 0.02, + msg.pos[1] + msg.force_direction[1] * 0.02, + msg.pos[2] + msg.force_direction[2] * 0.02, + ) + flash_color = (1.0, 0.8, 0.4) + light = bs.newnode( + 'light', + attrs={ + 'position': punchpos, + 'radius': 0.12 + hurtiness * 0.12, + 'intensity': 0.3 * (1.0 + 1.0 * hurtiness), + 'height_attenuated': False, + 'color': flash_color, + }, + ) + bs.timer(0.06, light.delete) + + flash = bs.newnode( + 'flash', + attrs={'position': punchpos, 'size': 0.17 + 0.17 * hurtiness, 'color': flash_color}, + ) + bs.timer(0.06, flash.delete) + + if msg.hit_type == 'impact': + assert msg.force_direction is not None + bs.emitfx( + position=msg.pos, + velocity=( + msg.force_direction[0] * 2.0, + msg.force_direction[1] * 2.0, + msg.force_direction[2] * 2.0, + ), + count=min(10, 1 + int(damage * 0.01)), + scale=0.4, + spread=0.1, + ) + if self.hitpoints > 0: + if msg.hit_type == 'impact' and damage > self.hitpoints: + newdamage = max(damage - 200, self.hitpoints - 10) + damage = newdamage + self.node.handlemessage('flash') + + if damage > 0.0 and self.node.hold_node: + self.node.hold_node = None + self.hitpoints -= damage + self.node.hurt = 1.0 - float(self.hitpoints) / self.hitpoints_max + + if self._cursed and damage > 0: + bs.timer( + 0.05, bs.WeakCallPartial(self.curse_explode, msg.get_source_player(bs.Player)) + ) + + if self.frozen and (damage > 200 or self.hitpoints <= 0): + self.shatter() + elif self.hitpoints <= 0: + self.node.handlemessage(bs.DieMessage(how=bs.DeathType.IMPACT)) + + if self.hitpoints <= 0: + damage_avg = self.node.damage_smoothed * damage_scale + if damage_avg > 1000: + self.shatter() + + elif isinstance(msg, BombDiedMessage): + self.bomb_count += 1 + + elif isinstance(msg, bs.DieMessage): + + def drop_bomb(): + for xbomb in range(3): + p = self.node.position + pos = (p[0] + xbomb, p[1] + 5, p[2] - xbomb) + ball = bomb.Bomb(position=pos, bomb_type='impact').autoretain() + ball.node.mesh_scale = 0.6 + ball.node.mesh = bs.getmesh('egg') + ball.node.gravity_scale = 2 + + if self.edg_eff: + self.edg_eff = False + + wasdead = self._dead + self._dead = True + self.hitpoints = 0 + if msg.immediate: + if self.node: + self.node.delete() + elif self.node: + self.node.hurt = 1.0 + if self.play_big_death_sound and not wasdead: + SpazFactory.get().single_player_death_sound.play() + self.node.dead = True + bs.timer(2.0, self.node.delete) + + t = 0 + if self.kill_eff: + for bombs in range(3): + bs.timer(t, babase.CallPartial(drop_bomb)) + t += 0.15 + self.kill_eff = False + + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage(how=bs.DeathType.FALL)) + + elif isinstance(msg, bs.StandMessage): + self._last_stand_pos = (msg.position[0], msg.position[1], msg.position[2]) + if self.node: + self.node.handlemessage( + 'stand', msg.position[0], msg.position[1], msg.position[2], msg.angle + ) + + elif isinstance(msg, CurseExplodeMessage): + self.curse_explode() + + elif isinstance(msg, PunchHitMessage): + if not self.node: + return None + node = bs.getcollision().opposingnode + + if node and (node not in self._punched_nodes): + + punch_momentum_angular = self.node.punch_momentum_angular * self._punch_power_scale + punch_power = self.node.punch_power * self._punch_power_scale + + if node.getnodetype() != 'spaz': + sounds = SpazFactory.get().impact_sounds_medium + sound = sounds[random.randrange(len(sounds))] + sound.play(1.0, self.node.position) + + ppos = self.node.punch_position + punchdir = self.node.punch_velocity + vel = self.node.punch_momentum_linear + + self._punched_nodes.add(node) + node.handlemessage( + bs.HitMessage( + pos=ppos, + velocity=vel, + magnitude=punch_power * punch_momentum_angular * 110.0, + velocity_magnitude=punch_power * 40, + radius=0, + srcnode=self.node, + source_player=self.source_player, + force_direction=punchdir, + hit_type='punch', + hit_subtype=('super_punch' if self._has_boxing_gloves else 'default'), + ) + ) + + mag = -400.0 + if self._hockey: + mag *= 0.5 + if len(self._punched_nodes) == 1: + self.node.handlemessage( + 'kick_back', + ppos[0], + ppos[1], + ppos[2], + punchdir[0], + punchdir[1], + punchdir[2], + mag, + ) + elif isinstance(msg, PickupMessage): + if not self.node: + return None + + try: + collision = bs.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + except bs.NotFoundError: + return True + + try: + if opposingnode.invincible: + return True + except Exception: + pass + + if ( + opposingnode.getnodetype() == 'spaz' + and not opposingnode.shattered + and opposingbody == 4 + ): + opposingbody = 1 + + held = self.node.hold_node + if held and held.getnodetype() == 'flag': + return True + + self.node.hold_body = opposingbody + self.node.hold_node = opposingnode + elif isinstance(msg, bs.CelebrateMessage): + if self.node: + self.node.handlemessage('celebrate', int(msg.duration * 1000)) + + return None + + +class PowerupManagerWindow(PopupWindow): + def __init__(self, transition='in_right'): + columns = 2 + self._width = width = 800 + self._height = height = 500 + self._sub_height = 200 + self._scroll_width = self._width * 0.90 + self._scroll_height = self._height - 180 + self._sub_width = self._scroll_width * 0.95 + self.tab_buttons: set = {} + self.list_cls_power: list = [] + self.default_powerups = default_powerups() + self.default_power_list = list(self.default_powerups) + self.coins = apg['Bear Coin'] + self.popup_cls_power = None + + if not STORE['Buy Firebombs']: + powerups['Fire Bombs'] = 0 + self.default_power_list.remove('Fire Bombs') + + self.charstr = [ + babase.charstr(babase.SpecialChar.LEFT_ARROW), + babase.charstr(babase.SpecialChar.RIGHT_ARROW), + babase.charstr(babase.SpecialChar.UP_ARROW), + babase.charstr(babase.SpecialChar.DOWN_ARROW), + ] + + self.tabdefs = { + "Action 1": ['powerupIceBombs', (1, 1, 1)], + "Action 2": ['settingsIcon', (0, 1, 0)], + "Action 3": ['inventoryIcon', (1, 1, 1)], + "Action 4": ['storeIcon', (1, 1, 1)], + "Action 5": ['advancedIcon', (1, 1, 1)], + "About": ['heart', (1.5, 0.3, 0.3)], + } + + if STORE['Buy Firebombs'] and STORE['Buy Option'] and STORE['Buy Percentage']: + self.tabdefs = { + "Action 1": ['powerupIceBombs', (1, 1, 1)], + "Action 2": ['settingsIcon', (0, 1, 0)], + "Action 3": ['inventoryIcon', (1, 1, 1)], + "About": ['heart', (1.5, 0.3, 0.3)], + } + + self.listdef = list(self.tabdefs) + + self.count = len(self.tabdefs) + + self._current_tab = GLOBAL['Tab'] + + app = bui.app.ui_v1 + uiscale = app.uiscale + + self._root_widget = bui.containerwidget( + size=(width + 90, height + 80), + transition=transition, + scale=1.5 if uiscale is babase.UIScale.SMALL else 1.0, + stack_offset=(0, -30) if uiscale is babase.UIScale.SMALL else (0, 0), + ) + + self._backButton = b = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(60, self._height - 15), + size=(130, 60), + scale=0.8, + text_scale=1.2, + label=babase.Lstr(resource='backText'), + button_type='back', + on_activate_call=babase.CallPartial(self._back), + ) + bui.buttonwidget( + edit=self._backButton, + button_type='backSmall', + size=(60, 60), + label=babase.charstr(babase.SpecialChar.BACK), + ) + bui.containerwidget(edit=self._root_widget, cancel_button=b) + + self.titletext = bui.textwidget( + parent=self._root_widget, + position=(0, height - 15), + size=(width, 50), + h_align="center", + color=bui.app.ui_v1.title_color, + v_align="center", + maxwidth=width * 1.3, + ) + + index = 0 + for tab in range(self.count): + for tab2 in range(columns): + + tag = self.listdef[index] + + position = (620 + (tab2 * 120), self._height - 50 * 2.5 - (tab * 120)) + + if tag == 'About': + text = babase.Lstr(resource='gatherWindow.aboutText') + elif tab == 'Action 4': + text = babase.Lstr(resource='storeText') + else: + text = getlanguage(tag) + + self.tab_buttons[tag] = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=position, + size=(110, 110), + scale=1, + label='', + enable_sound=False, + button_type='square', + on_activate_call=babase.CallPartial(self._set_tab, tag, sound=True), + ) + + self.text = bui.textwidget( + parent=self._root_widget, + position=(position[0] + 55, position[1] + 30), + size=(0, 0), + scale=1, + color=bui.app.ui_v1.title_color, + draw_controller=self.tab_buttons[tag], + maxwidth=100, + text=text, + h_align='center', + v_align='center', + ) + + self.image = bui.imagewidget( + parent=self._root_widget, + size=(60, 60), + color=self.tabdefs[tag][1], + draw_controller=self.tab_buttons[tag], + position=(position[0] + 25, position[1] + 40), + texture=bui.gettexture(self.tabdefs[tag][0]), + ) + + index += 1 + + if self.count == index: + break + + if self.count == index: + break + + self._scrollwidget = None + self._tab_container = None + self._set_tab(self._current_tab) + + def __del__(self): + apg.apply_and_commit() + + def popup_menu_closing(self, window): + print("saliendo") + + def _set_tab(self, tab, sound: bool = False): + self.sound = sound + GLOBAL['Tab'] = tab + apg.apply_and_commit() + + if self._tab_container is not None and self._tab_container.exists(): + self._tab_container.delete() + + if self.sound: + bui.getsound('swish').play() + + if self._scrollwidget: + self._scrollwidget.delete() + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=(self._width * 0.08, 51 * 1.8), + size=(self._sub_width - 140, self._scroll_height + 60 * 1.2), + ) + + if tab == 'Action 4': + if self._scrollwidget: + self._scrollwidget.delete() + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=(self._width * 0.08, 51 * 1.8), + size=(self._sub_width - 140, self._scroll_height + 60 * 1.2), + capture_arrows=True, + center_small_content=False, + selection_loops_to_parent=True, + claims_left_right=True, + claims_up_down=False, + color=(0.3, 0.3, 0.4), + ) + bui.textwidget(edit=self.titletext, text=babase.Lstr(resource='storeText')) + + elif tab == 'About': + bui.textwidget(edit=self.titletext, text=babase.Lstr(resource='gatherWindow.aboutText')) + else: + bui.textwidget(edit=self.titletext, text=getlanguage(tab)) + + choices = ['Reset', 'Only Bombs', 'Only Items', 'New', 'Nothing'] + c_display = [] + + for display in choices: + choices_display = babase.Lstr(translate=("", getlanguage(display))) + c_display.append(choices_display) + + if tab == 'Action 1': + self.popup_cls_power = PopupMenu( + parent=self._root_widget, + position=(130, self._width * 0.61), + button_size=(150, 50), + scale=2.5, + choices=choices, + width=150, + choices_display=c_display, + current_choice=GLOBAL['Cls Powerup'], + on_value_change_call=self._set_concept, + ) + self.list_cls_power.append(self.popup_cls_power._button) + + self.button_cls_power = bui.buttonwidget( + parent=self._root_widget, + position=(500, self._width * 0.61), + size=(50, 50), + autoselect=True, + scale=1, + label=('%'), + text_scale=1, + button_type='square', + on_activate_call=self._percentage_window, + ) + self.list_cls_power.append(self.button_cls_power) + + rewindow = [self.popup_cls_power._button, self.button_cls_power] + + for ( + cls + ) in self.list_cls_power: # this is very important so that pupups don't accumulate + if cls not in rewindow: + cls.delete() + + elif tab == 'Action 4': + self.button_coin = bui.buttonwidget( + parent=self._root_widget, + icon=bui.gettexture('coin'), + position=(550, self._width * 0.614), + size=(160, 40), + textcolor=(0, 1, 0), + color=(0, 1, 6), + scale=1, + label=str(apg['Bear Coin']), + text_scale=1, + autoselect=True, + on_activate_call=None, + ) # self._percentage_window) + self.list_cls_power.append(self.button_coin) + + try: + rewindow.append(self.button_coin) + except: + rewindow = [self.button_coin] + for ( + cls + ) in self.list_cls_power: # this is very important so that pupups don't accumulate + if cls not in rewindow: + cls.delete() + + else: + try: + for cls in self.list_cls_power: + cls.delete() + except: + pass + + if tab == 'Action 1': + sub_height = len(self.default_power_list) * 90 + v = sub_height - 55 + width = 300 + posi = 0 + id_power = list(self.default_powerups) + new_powerups = id_power[9:] + self.listpower = {} + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + for power in self.default_power_list: + if power == id_power[0]: + text = 'helpWindow.powerupShieldNameText' + tex = bui.gettexture('powerupShield') + elif power == id_power[1]: + text = 'helpWindow.powerupPunchNameText' + tex = bui.gettexture('powerupPunch') + elif power == id_power[2]: + text = 'helpWindow.powerupLandMinesNameText' + tex = bui.gettexture('powerupLandMines') + elif power == id_power[3]: + text = 'helpWindow.powerupImpactBombsNameText' + tex = bui.gettexture('powerupImpactBombs') + elif power == id_power[4]: + text = 'helpWindow.powerupIceBombsNameText' + tex = bui.gettexture('powerupIceBombs') + elif power == id_power[5]: + text = 'helpWindow.powerupBombNameText' + tex = bui.gettexture('powerupBomb') + elif power == id_power[6]: + text = 'helpWindow.powerupStickyBombsNameText' + tex = bui.gettexture('powerupStickyBombs') + elif power == id_power[7]: + text = 'helpWindow.powerupCurseNameText' + tex = bui.gettexture('powerupCurse') + elif power == id_power[8]: + text = 'helpWindow.powerupHealthNameText' + tex = bui.gettexture('powerupHealth') + elif power == id_power[9]: + text = power + tex = bui.gettexture('powerupSpeed') + elif power == id_power[10]: + text = power + tex = bui.gettexture('heart') + elif power == id_power[11]: + text = "Goodbye!" + tex = bui.gettexture('achievementOnslaught') + elif power == id_power[12]: + text = power + tex = bui.gettexture('ouyaUButton') + elif power == id_power[13]: + text = power + tex = bui.gettexture('achievementSuperPunch') + elif power == id_power[14]: + text = power + tex = bui.gettexture('levelIcon') + elif power == id_power[15]: + text = power + tex = bui.gettexture('ouyaOButton') + elif power == id_power[16]: + text = power + tex = bui.gettexture('star') + + if power in new_powerups: + label = getlanguage(power) + else: + label = babase.Lstr(resource=text) + + apperance = powerups[power] + position = (90, v - posi) + + t = bui.textwidget( + parent=c, + position=(position[0] - 30, position[1] - 15), + size=(width, 50), + h_align="center", + color=(bui.app.ui_v1.title_color), + text=label, + v_align="center", + maxwidth=width * 1.3, + ) + + self.powprev = bui.imagewidget( + parent=c, + position=(position[0] - 70, position[1] - 10), + size=(50, 50), + texture=tex, + ) + + dipos = 0 + for direc in ['-', '+']: + bui.buttonwidget( + parent=c, + autoselect=True, + position=(position[0] + 270 + dipos, position[1] - 10), + size=(100, 100), + scale=0.4, + label=direc, + button_type='square', + text_scale=4, + on_activate_call=babase.CallPartial(self.apperance_powerups, power, direc), + ) + + dipos += 100 + + textwidget = bui.textwidget( + parent=c, + position=(position[0] + 190, position[1] - 15), + size=(width, 50), + h_align="center", + color=cls_pow_color()[apperance], + text=str(apperance), + v_align="center", + maxwidth=width * 1.3, + ) + self.listpower[power] = textwidget + + posi += 90 + + elif tab == 'Action 2': + sub_height = 370 if not STORE['Buy Option'] else 450 + v = sub_height - 55 + width = 300 + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + position = (40, v - 20) + + c_display = [] + choices = ['Auto', 'SY: BALL', 'SY: Impact', 'SY: Egg'] + for display in choices: + choices_display = babase.Lstr(translate=("", getlanguage(display))) + c_display.append(choices_display) + + popup = PopupMenu( + parent=c, + position=(position[0] + 300, position[1]), + button_size=(150, 50), + scale=2.5, + choices=choices, + width=150, + choices_display=c_display, + current_choice=config['Powerup Style'], + on_value_change_call=babase.CallPartial(self._all_popup, 'Powerup Style'), + ) + + text = getlanguage('Powerup Style') + wt = len(text) * 0.80 + t = bui.textwidget( + parent=c, + position=(position[0] - 60 + wt, position[1]), + size=(width, 50), + maxwidth=width * 0.9, + scale=1.1, + h_align="center", + color=bui.app.ui_v1.title_color, + text=getlanguage('Powerup Style'), + v_align="center", + ) + + dipos = 0 + for direc in ['-', '+']: + bui.buttonwidget( + parent=c, + autoselect=True, + position=(position[0] + 310 + dipos, position[1] - 100), + size=(100, 100), + repeat=True, + scale=0.4, + label=direc, + button_type='square', + text_scale=4, + on_activate_call=babase.CallPartial(self._powerups_scale, direc), + ) + dipos += 100 + + txt_scale = config['Powerup Scale'] + self.txt_scale = bui.textwidget( + parent=c, + position=(position[0] + 230, position[1] - 105), + size=(width, 50), + scale=1.1, + h_align="center", + color=(0, 1, 0), + text=str(txt_scale), + v_align="center", + maxwidth=width * 1.3, + ) + + text = getlanguage('Powerup Scale') + wt = len(text) * 0.80 + t = bui.textwidget( + parent=c, + position=(position[0] - 60 + wt, position[1] - 100), + size=(width, 50), + maxwidth=width * 0.9, + scale=1.1, + h_align="center", + color=bui.app.ui_v1.title_color, + text=text, + v_align="center", + ) + + position = (position[0] - 20, position[1] + 40) + + self.check = bui.checkboxwidget( + parent=c, + position=(position[0] + 30, position[1] - 230), + value=config['Powerup Name'], + on_value_change_call=babase.CallPartial(self._switches, 'Powerup Name'), + maxwidth=self._scroll_width * 0.9, + text=getlanguage('Powerup Name'), + autoselect=True, + ) + + self.check = bui.checkboxwidget( + parent=c, + position=(position[0] + 30, position[1] - 230 * 1.3), + value=config['Powerup With Shield'], + on_value_change_call=babase.CallPartial(self._switches, 'Powerup With Shield'), + maxwidth=self._scroll_width * 0.9, + text=getlanguage('Powerup With Shield'), + autoselect=True, + ) + + if STORE['Buy Option']: + self.check = bui.checkboxwidget( + parent=c, + position=(position[0] + 30, position[1] - 230 * 1.6), + value=config['Powerup Time'], + on_value_change_call=babase.CallPartial(self._switches, 'Powerup Time'), + maxwidth=self._scroll_width * 0.9, + text=getlanguage('Powerup Time'), + autoselect=True, + ) + + elif tab == 'Action 3': + sub_height = 300 + v = sub_height - 55 + width = 300 + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + v -= 20 + position = (110, v - 45 * 1.72) + + if not STORE['Buy Percentage']: + t = bui.textwidget( + parent=c, + position=(90, v - 100), + size=(30 + width, 50), + h_align="center", + text=getlanguage('Block Option Store'), + color=bui.app.ui_v1.title_color, + v_align="center", + maxwidth=width * 1.5, + scale=1.5, + ) + + i = bui.imagewidget( + parent=c, + position=(position[0] + 100, position[1] - 205), + size=(80, 80), + texture=bui.gettexture('lock'), + ) + else: + t = bui.textwidget( + parent=c, + position=(position[0] - 14, position[1] + 70), + size=(30 + width, 50), + h_align="center", + text=f"{getlanguage('Tank Shield PTG')} ({getlanguage('Tank Shield')})", + color=bui.app.ui_v1.title_color, + v_align="center", + maxwidth=width * 1.5, + scale=1.5, + ) + + b = bui.buttonwidget( + parent=c, + autoselect=True, + position=position, + size=(100, 100), + repeat=True, + scale=0.6, + label=self.charstr[3], + button_type='square', + text_scale=2, + on_activate_call=babase.CallPartial(self.tank_shield_percentage, 'Decrement'), + ) + + b = bui.buttonwidget( + parent=c, + autoselect=True, + repeat=True, + text_scale=2, + position=(position[0] * 3.2, position[1]), + size=(100, 100), + scale=0.6, + label=self.charstr[2], + button_type='square', + on_activate_call=babase.CallPartial(self.tank_shield_percentage, 'Increment'), + ) + + porcentaje = config['Tank Shield PTG'] + if porcentaje > 59: + color = (0, 1, 0) + elif porcentaje < 40: + color = (1, 1, 0) + else: + color = (0, 1, 0.8) + + self.tank_text = bui.textwidget( + parent=c, + position=(position[0] - 14, position[1] + 5), + size=(30 + width, 50), + h_align="center", + text=str(porcentaje) + '%', + color=color, + v_align="center", + maxwidth=width * 1.3, + scale=2, + ) + + # -----> + + position = (110, v - 160 * 1.6) + t = bui.textwidget( + parent=c, + position=(position[0] - 14, position[1] + 70), + size=(30 + width, 50), + h_align="center", + text=f"{getlanguage('Healing Damage PTG')}{_sp_}({getlanguage('Healing Damage')})", + color=bui.app.ui_v1.title_color, + v_align="center", + maxwidth=width * 1.3, + scale=1.4, + ) + + b = bui.buttonwidget( + parent=c, + autoselect=True, + position=position, + size=(100, 100), + repeat=True, + scale=0.6, + label=self.charstr[3], + button_type='square', + text_scale=2, + on_activate_call=babase.CallPartial(self.health_damage_percentage, 'Decrement'), + ) + + b = bui.buttonwidget( + parent=c, + autoselect=True, + repeat=True, + text_scale=2, + position=(position[0] * 3.2, position[1]), + size=(100, 100), + scale=0.6, + label=self.charstr[2], + button_type='square', + on_activate_call=babase.CallPartial(self.health_damage_percentage, 'Increment'), + ) + + porcentaje = config['Healing Damage PTG'] + if porcentaje > 59: + color = (0, 1, 0) + elif porcentaje < 40: + color = (1, 1, 0) + else: + color = (0, 1, 0.8) + + self.hlg_text = bui.textwidget( + parent=c, + position=(position[0] - 14, position[1] + 5), + size=(30 + width, 50), + h_align="center", + text=str(porcentaje) + '%', + color=color, + v_align="center", + maxwidth=width * 1.3, + scale=2, + ) + + elif tab == 'Percentage': + sub_height = len(self.default_power_list) * 90 + v = sub_height - 55 + width = 300 + posi = 0 + id_power = list(self.default_powerups) + new_powerups = id_power[9:] + self.listpower = {} + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + for power in self.default_power_list: + if power == id_power[0]: + text = 'helpWindow.powerupShieldNameText' + tex = bui.gettexture('powerupShield') + elif power == id_power[1]: + text = 'helpWindow.powerupPunchNameText' + tex = bui.gettexture('powerupPunch') + elif power == id_power[2]: + text = 'helpWindow.powerupLandMinesNameText' + tex = bui.gettexture('powerupLandMines') + elif power == id_power[3]: + text = 'helpWindow.powerupImpactBombsNameText' + tex = bui.gettexture('powerupImpactBombs') + elif power == id_power[4]: + text = 'helpWindow.powerupIceBombsNameText' + tex = bui.gettexture('powerupIceBombs') + elif power == id_power[5]: + text = 'helpWindow.powerupBombNameText' + tex = bui.gettexture('powerupBomb') + elif power == id_power[6]: + text = 'helpWindow.powerupStickyBombsNameText' + tex = bui.gettexture('powerupStickyBombs') + elif power == id_power[7]: + text = 'helpWindow.powerupCurseNameText' + tex = bui.gettexture('powerupCurse') + elif power == id_power[8]: + text = 'helpWindow.powerupHealthNameText' + tex = bui.gettexture('powerupHealth') + elif power == id_power[9]: + text = power + tex = bui.gettexture('powerupSpeed') + elif power == id_power[10]: + text = power + tex = bui.gettexture('heart') + elif power == id_power[11]: + text = "Goodbye!" + tex = bui.gettexture('achievementOnslaught') + elif power == id_power[12]: + text = power + tex = bui.gettexture('ouyaUButton') + elif power == id_power[13]: + text = power + tex = bui.gettexture('achievementSuperPunch') + elif power == id_power[14]: + text = power + tex = bui.gettexture('levelIcon') + elif power == id_power[15]: + text = power + tex = bui.gettexture('ouyaOButton') + elif power == id_power[16]: + text = power + tex = bui.gettexture('star') + + if power in new_powerups: + label = getlanguage(power) + else: + label = babase.Lstr(resource=text) + + apperance = powerups[power] + position = (90, v - posi) + + t = bui.textwidget( + parent=c, + position=(position[0] - 30, position[1] - 15), + size=(width, 50), + h_align="center", + color=(bui.app.ui_v1.title_color), + text=label, + v_align="center", + maxwidth=width * 1.3, + ) + + self.powprev = bui.imagewidget( + parent=c, + position=(position[0] - 70, position[1] - 10), + size=(50, 50), + texture=tex, + ) + + ptg = str(self.total_percentage(power)) + t = bui.textwidget( + parent=c, + position=(position[0] + 170, position[1] - 10), + size=(width, 50), + h_align="center", + color=(0, 1, 0), + text=(f'{ptg}%'), + v_align="center", + maxwidth=width * 1.3, + ) + + posi += 90 + + elif tab == 'Action 4': + sub_height = 370 + width = 300 + v = sub_height - 55 + u = width - 60 + + if not self._scrollwidget or not self._scrollwidget.exists(): + return + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(width + 500, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + position = (u + 150, v - 250) + n_pos = 0 + prices = [7560, 5150, 3360] + str_name = ["FireBombs Store", "Timer Store", "Percentages Store"] + images = ["ouyaOButton", "settingsIcon", "inventoryIcon"] + + index = 0 + for store in store_items(): + p = prices[index] + txt = str_name[index] + label = getlanguage(txt) + tx_pos = len(label) * 1.8 + lb_scale = len(label) * 0.20 + preview = images[index] + + if STORE[store]: + text = getlanguage('Bought') + icon = bui.gettexture('graphicsIcon') + color = (0.52, 0.48, 0.63) + txt_scale = 1.5 + else: + text = str(p) + icon = bui.gettexture('coin') + color = (0.5, 0.4, 0.93) + txt_scale = 2 + + b = bui.buttonwidget( + parent=c, + autoselect=True, + position=(position[0] + 210 - n_pos, position[1]), + size=(250, 80), + scale=0.7, + label=text, + text_scale=txt_scale, + icon=icon, + color=color, + iconscale=1.7, + on_activate_call=babase.CallPartial(self._buy_object, store, p), + ) + + s = 180 + b = bui.buttonwidget( + parent=c, + autoselect=True, + position=(position[0] + 210 - n_pos, position[1] + 55), + size=(s, s + 30), + scale=1, + label='', + color=color, + button_type='square', + on_activate_call=babase.CallPartial(self._buy_object, store, p), + ) + + s -= 80 + i = bui.imagewidget( + parent=c, + draw_controller=b, + position=(position[0] + 250 - n_pos, position[1] + 140), + size=(s, s), + texture=bui.gettexture(preview), + ) + + t = bui.textwidget( + parent=c, + position=(position[0] + 270 - n_pos, position[1] + 101), + h_align="center", + color=(bui.app.ui_v1.title_color), + text=label, + v_align="center", + maxwidth=130, + ) + + n_pos += 280 + index += 1 + + elif tab == 'Action 5': + sub_height = 370 + v = sub_height - 55 + width = 300 + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + position = (0, v - 30) + + t = bui.textwidget( + parent=c, + position=(position[0] + 80, position[1] - 30), + size=(width + 60, 50), + scale=1, + h_align="center", + color=(bui.app.ui_v1.title_color), + text=babase.Lstr(resource='settingsWindowAdvanced.enterPromoCodeText'), + v_align="center", + maxwidth=width * 1.3, + ) + + self.promocode_text = bui.textwidget( + parent=c, + position=(position[0] + 80, position[1] - 100), + size=(width + 60, 50), + scale=1, + editable=True, + h_align="center", + color=(bui.app.ui_v1.title_color), + text='', + v_align="center", + maxwidth=width * 1.3, + max_chars=30, + description=babase.Lstr(resource='settingsWindowAdvanced.enterPromoCodeText'), + ) + + self.promocode_button = bui.buttonwidget( + parent=c, + position=(position[0] + 160, position[1] - 170), + size=(200, 60), + scale=1.0, + label=babase.Lstr(resource='submitText'), + on_activate_call=self._promocode, + ) + + else: + sub_height = 0 + v = sub_height - 55 + width = 300 + + self._tab_container = c = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, sub_height), + background=False, + selection_loops_to_parent=True, + ) + + t = bui.textwidget( + parent=c, + position=(110, v - 20), + size=(width, 50), + scale=1.4, + color=(0.2, 1.2, 0.2), + h_align="center", + v_align="center", + text=("Ultimate Powerup Manager v2.5"), + maxwidth=width * 30, + ) + + t = bui.textwidget( + parent=c, + position=(110, v - 90), + size=(width, 50), + scale=1, + color=(1.3, 0.5, 1.0), + h_align="center", + v_align="center", + text=getlanguage('Creator'), + maxwidth=width * 30, + ) + + t = bui.textwidget( + parent=c, + position=(110, v - 220), + size=(width, 50), + scale=1, + color=(1.0, 1.2, 0.3), + h_align="center", + v_align="center", + text=getlanguage('Mod Info'), + maxwidth=width * 30, + ) + + for select_tab, button_tab in self.tab_buttons.items(): + if select_tab == tab: + bui.buttonwidget(edit=button_tab, color=(0.5, 0.4, 1.5)) + else: + bui.buttonwidget(edit=button_tab, color=(0.52, 0.48, 0.63)) + + def _all_popup(self, tag: str, popup: str) -> None: + config[tag] = popup + apg.apply_and_commit() + + def _set_concept(self, concept: str) -> None: + GLOBAL['Cls Powerup'] = concept + + if concept == 'Reset': + for power, deflt in default_powerups().items(): + powerups[power] = deflt + elif concept == 'Nothing': + for power in default_powerups(): + powerups[power] = 0 + elif concept == 'Only Bombs': + for power, deflt in default_powerups().items(): + if 'Bombs' not in power: + powerups[power] = 0 + else: + powerups[power] = 3 + elif concept == 'Only Items': + for power, deflt in default_powerups().items(): + if 'Bombs' in power: + powerups[power] = 0 + else: + powerups[power] = deflt + elif concept == 'New': + default_power = default_powerups() + new_powerups = list(default_power)[9:] + for power, deflt in default_power.items(): + if power not in new_powerups: + powerups[power] = 0 + else: + powerups[power] = deflt + + if not STORE['Buy Firebombs']: + powerups['Fire Bombs'] = 0 + + self._set_tab('Action 1') + + def tank_shield_percentage(self, tag): + max = 96 + min = 40 + if tag == 'Increment': + config['Tank Shield PTG'] += 1 + if config['Tank Shield PTG'] > max: + config['Tank Shield PTG'] = min + elif tag == 'Decrement': + config['Tank Shield PTG'] -= 1 + if config['Tank Shield PTG'] < min: + config['Tank Shield PTG'] = max + + porcentaje = config['Tank Shield PTG'] + if porcentaje > 59: + color = (0, 1, 0) + elif porcentaje < 40: + color = (1, 1, 0) + else: + color = (0, 1, 0.8) + bui.textwidget(edit=self.tank_text, text=str(porcentaje) + '%', color=color) + + def health_damage_percentage(self, tag): + max = 80 + min = 35 + if tag == 'Increment': + config['Healing Damage PTG'] += 1 + if config['Healing Damage PTG'] > max: + config['Healing Damage PTG'] = min + elif tag == 'Decrement': + config['Healing Damage PTG'] -= 1 + if config['Healing Damage PTG'] < min: + config['Healing Damage PTG'] = max + + porcentaje = config['Healing Damage PTG'] + if porcentaje > 59: + color = (0, 1, 0) + elif porcentaje < 40: + color = (1, 1, 0) + else: + color = (0, 1, 0.8) + bui.textwidget(edit=self.hlg_text, text=str(porcentaje) + '%', color=color) + + def apperance_powerups(self, powerup: str, ID: str): + max = 7 + if ID == "-": + if powerups[powerup] == 0: + powerups[powerup] = max + else: + powerups[powerup] -= 1 + elif ID == "+": + if powerups[powerup] == max: + powerups[powerup] = 0 + else: + powerups[powerup] += 1 + enum = powerups[powerup] + bui.textwidget( + edit=self.listpower[powerup], text=str(powerups[powerup]), color=cls_pow_color()[enum] + ) + + def _powerups_scale(self, ID: str): + max = 1.5 + min = 0.5 + sc = 0.1 + if ID == "-": + if config['Powerup Scale'] < (min + 0.1): + config['Powerup Scale'] = max + else: + config['Powerup Scale'] -= sc + elif ID == "+": + if config['Powerup Scale'] > (max - 0.1): + config['Powerup Scale'] = min + else: + config['Powerup Scale'] += sc + config['Powerup Scale'] = round(config['Powerup Scale'], 1) + bui.textwidget(edit=self.txt_scale, text=str(config['Powerup Scale'])) + + def total_percentage(self, power): + total = 0 + pw = powerups[power] + for i, i2 in powerups.items(): + total += i2 + if total == 0: + return float(total) + else: + ptg = 100 * pw / total + result = round(ptg, 2) + return result + + def store_refresh(self, tag: str): + if tag == 'Buy Firebombs': + powerups['Fire Bombs'] = 3 + self.default_power_list.append('Fire Bombs') + self._set_tab('Action 4') + + def _buy_object(self, tag: str, price: int): + store = BearStore( + value=tag, price=price, callback=babase.CallPartial(self.store_refresh, tag) + ) + store.buy() + + def _promocode(self): + code = bui.textwidget(query=self.promocode_text) + promo = PromoCode(code=code) + promo.code_confirmation() + bui.textwidget(edit=self.promocode_text, text="") + + def _switches(self, tag, m): + config[tag] = False if m == 0 else True + apg.apply_and_commit() + + def _percentage_window(self): + self._set_tab('Percentage') + + def _back(self): + bui.containerwidget(edit=self._root_widget, transition='out_left') + babase.app.classic.profile_browser_window() + + +def add_plugin(): + try: + from baBearModz import BearPlugin + except Exception as e: + return bs.timer(2.5, lambda e=e: bs.broadcastmessage('Error plugin: ' + str(e), (1, 0, 0))) + BearPlugin( + icon='logo', + creator='UPDATE TO API 9 BY ATD(anas) and less', + button_color=(1, 1, 0), + plugin=UltimatePowerupManager, + window=PowerupManagerWindow, + ) + + +# ba_meta export babase.Plugin + + +class UltimatePowerupManager(babase.Plugin): + # browser.ProfileBrowserWindow = NewProfileBrowserWindow + pupbox.PowerupBoxFactory = NewPowerupBoxFactory + pupbox.PowerupBox.__init__ = _pbx_ + Bomb.__init__ = _bomb_init + SpazBot.handlemessage = bot_handlemessage + Blast.handlemessage = bomb_handlemessage + Spaz.handlemessage = new_handlemessage + Spaz.__init__ = _init_spaz_ + Spaz._get_bomb_type_tex = new_get_bomb_type_tex + Spaz.on_punch_press = spaz_on_punch_press + Spaz.on_punch_release = spaz_on_punch_release + MainMenuActivity.on_transition_in = new_on_transition_in + + def __init__(self) -> None: + + # add_plugin() + ... + + def has_settings_ui(self): + return True + + def show_settings_ui(self, origin_widget): + PowerupManagerWindow() diff --git a/plugins/utilities/practice_tools.py b/plugins/utilities/practice_tools.py new file mode 100644 index 000000000..632597e1a --- /dev/null +++ b/plugins/utilities/practice_tools.py @@ -0,0 +1,2419 @@ +"""Practice Tools Mod: V3.0 +Made by Cross Joy""" + +# If anyone wants to help me by giving suggestions/fixing bugs/ creating PR, +# Can visit my GitHub https://github.com/CrossJoy/Bombsquad-Modding + +# You can contact me through Discord: +# My Discord ID: Cross Joy#0721 +# My BS Discord Server: https://discord.gg/JyBY6haARJ + +# I would appreciate some support. :') +# Support link: https://www.buymeacoffee.com/CrossJoy + +# ---------------------------------------------------------------------------- + +# V3.0 update +# - Updated to Api 9 +# - Fixed the bomb radius visual not properly disappearing after being disabled. + +# ---------------------------------------------------------------------------- +# Powerful and comprehensive tools for practice purposes. + +# Features: +# - Spawn any bot anywhere. +# - Can spawn power-ups on your own. +# - Bomb radius visualizer. (Thx Mikirog for some of the codes :D ) +# - Bomb Countdown. +# and many more + +# Go explore the tools yourself.:) + +# Practice tabs can be accessed through the party window. +# Coop and local multiplayer compatible. +# Work on any 1.79+ ver. + +# FAQ: +# Can I use it to practice with friends? +# - Yes, but you are the only one who can access the practice window. + +# Does it work when I join a public server? +# - Not possible. + +# Can I use it during Co-op game? +# - Yes, it works fine. +# ---------------------------------------------------------------------------- + +from __future__ import annotations + +import math +import random +import weakref +from enum import Enum +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +import bascenev1lib +import bauiv1 as bui +from bauiv1lib import mainmenu +from babase import app, Plugin +from bascenev1lib.actor.powerupbox import PowerupBox, PowerupBoxFactory +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.spaz import Spaz +from bascenev1lib.actor import spawner +from bascenev1lib.actor.spazbot import (SpazBotSet, SpazBot, BrawlerBot, + TriggerBot, + ChargerBot, StickyBot, ExplodeyBot, + BouncyBot, + BomberBotPro, BrawlerBotPro, + TriggerBotPro, + ChargerBotPro, BomberBotProShielded, + BrawlerBotProShielded, + TriggerBotProShielded, + ChargerBotProShielded, BomberBotLite, + BrawlerBotLite) +from bascenev1lib.mainmenu import MainMenuSession +from bauiv1lib import popup +from bauiv1lib.party import PartyWindow as OriginalPartyWindow +from bauiv1lib.tabs import TabRow + +if TYPE_CHECKING: + from typing import Any, Sequence, Callable, Optional + +version = '3.0' + + +class ConfigLoader: + def __init__(self): + self.config_data = { + "Practice Tab": False, + "bombCountdown": False, + "bombRadiusVisual": False, + "stopBots": False, + "immortalDummy": False, + "powerupsExpire": False, + "invincible": False + } + self.config_names = ["Practice Tab", "bombCountdown", + "bombRadiusVisual", "stopBots", + "immortalDummy", "powerupsExpire", "invincible"] + + def load_configs(self): + for config_name in self.config_names: + try: + existing_config = babase.app.config.get(config_name) + if existing_config is None: + babase.app.config[config_name] = self.config_data[ + config_name] + else: + babase.app.config.get(config_name) + except: + babase.app.config[config_name] = self.config_data[config_name] + + +class PartyWindow(bui.Window): + _redefine_methods = ['__init__'] + + def __init__(self, *args, **kwargs): + getattr(self, '__init___old')(*args, **kwargs) + + self.bg_color = (.5, .5, .5) + + self._edit_movements_button = bui.buttonwidget( + parent=self._root_widget, + scale=0.7, + position=(360, self._height - 47), + # (self._width - 80, self._height - 47) + size=(100, 50), + label='Practice', + autoselect=True, + button_type='square', + on_activate_call=bs.Call(doTestButton, self), + color=self.bg_color, + iconscale=1.2) + + +def redefine(obj: object, name: str, new: callable, + new_name: str = None) -> None: + if not new_name: + new_name = name + '_old' + if hasattr(obj, name): + setattr(obj, new_name, getattr(obj, name)) + setattr(obj, name, new) + + +def redefine_class(original_cls: object, cls: object) -> None: + for method in cls._redefine_methods: + redefine(original_cls, method, getattr(cls, method)) + + +def main(plugin: Plugin) -> None: + print(f'Practice Tools v{plugin.__version__}') + app.practice_tool = plugin + redefine_class(OriginalPartyWindow, PartyWindow) + + +class NewMainMenuWindow(mainmenu.MainMenuWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Display chat icon, but if user open/close gather it may disappear + bui.set_party_window_open(True) + + +# ba_meta require api 9 +# ba_meta export plugin +class Practice(Plugin): + __version__ = version + + def on_app_running(self) -> None: + """Plugin start point.""" + + if app.env.engine_build_number < 20427: + bui.screenmessage( + 'ok', + color=(.8, .1, .1)) + raise RuntimeError( + 'sad') + mainmenu.MainMenuWindow = NewMainMenuWindow + config_loader = ConfigLoader() + config_loader.load_configs() + return main(self) + + +def new_bomb_init(func): + def setting(*args, **kwargs): + func(*args, **kwargs) + + bomb_type = args[0].bomb_type + fuse_bomb = ('land_mine', 'tnt', 'impact') + + args[0].radius_visualizer = bs.newnode('locator', + owner=args[0].node, + # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circle', + 'color': (1, 0, 0), + 'opacity': 0.05, + 'draw_beauty': False, + 'additive': False + }) + args[0].node.connectattr('position', args[0].radius_visualizer, + 'position') + + args[0].radius_visualizer_circle = bs.newnode( + 'locator', + owner=args[ + 0].node, + # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circleOutline', + 'size': [ + args[ + 0].blast_radius * 2.0], + # Here's that bomb's blast radius value again! + 'color': ( + 1, 1, 0), + 'draw_beauty': False, + 'additive': True + }) + args[0].node.connectattr('position', + args[0].radius_visualizer_circle, + 'position') + + if bomb_type == 'tnt': + args[0].fatal = bs.newnode('locator', + owner=args[0].node, + # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circle', + 'color': ( + 0.7, 0, 0), + 'opacity': 0.10, + 'draw_beauty': False, + 'additive': False + }) + args[0].node.connectattr('position', + args[0].fatal, + 'position') + + update_bomb_visual(args[0]) + + if babase.app.config.get( + "bombCountdown") and bomb_type not in fuse_bomb: + color = (1.0, 1.0, 0.0) + count_bomb(*args, count='3', color=color) + color = (1.0, 0.5, 0.0) + bs.timer(1, bs.Call(count_bomb, *args, count='2', color=color)) + color = (1.0, 0.15, 0.15) + bs.timer(2, bs.Call(count_bomb, *args, count='1', color=color)) + + return setting + + +def update_bomb_visual(bomb): + + if babase.app.config.get("bombRadiusVisual"): + bs.animate_array(bomb.radius_visualizer, 'size', 1, { + 0.0: [0.0], + 0.2: [bomb.blast_radius * 2.2], + 0.25: [bomb.blast_radius * 2.0] + }) + + bs.animate( + bomb.radius_visualizer_circle, 'opacity', { + 0: 0.0, + 0.4: 0.1 + }) + if bomb.bomb_type == 'tnt': + bs.animate_array(bomb.fatal, 'size', 1, { + 0.0: [0.0], + 0.2: [bomb.blast_radius * 2.2 * 0.7], + 0.25: [bomb.blast_radius * 2.0 * 0.7] + }) + else: + bs.animate_array(bomb.radius_visualizer, 'size', 1, { + 0.0: [0.0], + 0.2: [0.0], + }) + + bs.animate( + bomb.radius_visualizer_circle, 'opacity', { + 0: 0.0, + 0.4: 0.0 + }) + if bomb.bomb_type == 'tnt': + bs.animate_array(bomb.fatal, 'size', 1, { + 0.0: [0.0], + 0.2: [0.0], + }) + + +bascenev1lib.actor.bomb.Bomb.__init__ = new_bomb_init( + bascenev1lib.actor.bomb.Bomb.__init__) + + +def _init_spaz_(func): + def wrapper(*args, **kwargs): + func(*args, **kwargs) + args[0].bot_radius = bs.newnode('locator', + owner=args[0].node, + attrs={ + 'shape': 'circle', + 'color': (0, 0, 1), + 'opacity': 0.0, + 'draw_beauty': False, + 'additive': False + }) + args[0].node.connectattr('position', + args[0].bot_radius, + 'position') + + args[0].radius_visualizer_circle = bs.newnode( + 'locator', + owner=args[0].node, + attrs={ + 'shape': 'circleOutline', + 'size': [(args[0].hitpoints_max - args[0].hitpoints) * 0.0048], + 'color': (0, 1, 1), + 'draw_beauty': False, + 'additive': True + }) + + args[0].node.connectattr('position', args[0].radius_visualizer_circle, + 'position') + + args[0].curse_visualizer = bs.newnode('locator', + owner=args[0].node, + attrs={ + 'shape': 'circle', + 'color': (1, 0, 0), + 'size': (0.0, 0.0, 0.0), + 'opacity': 0.05, + 'draw_beauty': False, + 'additive': False + }) + args[0].node.connectattr('position', args[0].curse_visualizer, + 'position') + + args[0].curse_visualizer_circle = bs.newnode( + 'locator', + owner=args[0].node, + attrs={ + 'shape': 'circleOutline', + 'size': [3 * 2.0], + 'color': ( + 1, 1, 0), + 'opacity': 0.0, + 'draw_beauty': False, + 'additive': True + }) + args[0].node.connectattr('position', + args[0].curse_visualizer_circle, + 'position') + + args[0].curse_visualizer_fatal = bs.newnode('locator', + owner=args[0].node, + attrs={ + 'shape': 'circle', + 'color': ( + 0.7, 0, 0), + 'size': (0.0, 0.0, 0.0), + 'opacity': 0.10, + 'draw_beauty': False, + 'additive': False + }) + args[0].node.connectattr('position', + args[0].curse_visualizer_fatal, + 'position') + + def invincible() -> None: + for i in bs.get_foreground_host_activity().players: + try: + if i.node: + if babase.app.config.get("invincible"): + i.actor.node.invincible = True + else: + i.actor.node.invincible = False + except: + pass + + bs.timer(1.001, bs.Call(invincible)) + + return wrapper + + +Spaz.__init__ = _init_spaz_(Spaz.__init__) + + +def new_cursed(func): + def wrapper(*args, **kwargs): + if args[0].node.invincible: + return + func(*args, **kwargs) + if babase.app.config.get("bombRadiusVisual"): + bs.animate_array(args[0].curse_visualizer, 'size', 1, { + 0.0: [0.0], + 0.2: [3 * 2.2], + 0.5: [3 * 2.0], + 5.0: [3 * 2.0], + 5.1: [0.0], + }) + + bs.animate( + args[0].curse_visualizer_circle, 'opacity', { + 0: 0.0, + 0.4: 0.1, + 5.0: 0.1, + 5.1: 0.0, + }) + + bs.animate_array(args[0].curse_visualizer_fatal, 'size', 1, { + 0.0: [0.0], + 0.2: [2.2], + 0.5: [2.0], + 5.0: [2.0], + 5.1: [0.0], + }) + + return wrapper + + +Spaz.curse = new_cursed(Spaz.curse) + + +def bot_handlemessage(func): + def wrapper(*args, **kwargs): + func(*args, **kwargs) + if isinstance(args[1], bs.PowerupMessage): + if args[1].poweruptype == 'health': + if babase.app.config.get("bombRadiusVisual"): + update_hit_visual(args[0]) + + if not (babase.app.config.get("powerupsExpire") and + args[0].powerups_expire): + if args[1].poweruptype == 'triple_bombs': + tex = PowerupBoxFactory.get().tex_bomb + args[0]._flash_billboard(tex) + args[0].set_bomb_count(3) + + elif args[1].poweruptype == 'impact_bombs': + args[0].bomb_type = 'impact' + tex = args[0]._get_bomb_type_tex() + args[0]._flash_billboard(tex) + + elif args[1].poweruptype == 'sticky_bombs': + args[0].bomb_type = 'sticky' + tex = args[0]._get_bomb_type_tex() + args[0]._flash_billboard(tex) + + elif args[1].poweruptype == 'punch': + tex = PowerupBoxFactory.get().tex_punch + args[0]._flash_billboard(tex) + args[0].equip_boxing_gloves() + + elif args[1].poweruptype == 'ice_bombs': + args[0].bomb_type = 'ice' + tex = args[0]._get_bomb_type_tex() + args[0]._flash_billboard(tex) + + if args[1].poweruptype in ['triple_bombs', 'impact_bombs', + 'sticky_bombs', 'punch', + 'ice_bombs']: + args[0].node.handlemessage('flash') + if args[1].sourcenode: + args[1].sourcenode.handlemessage( + bs.PowerupAcceptMessage()) + return True + + if isinstance(args[1], bs.HitMessage): + update_hit_visual(args[0]) + + return wrapper + + +def update_hit_visual(node): + if node._cursed: + bs.animate_array(node.curse_visualizer, 'size', 1, { + 0.0: [3 * 2.0], + 0.2: [0.0], + }) + + bs.animate( + node.curse_visualizer_circle, 'opacity', { + 0.0: 0.1, + 0.2: 0.0, + }) + + bs.animate_array(node.curse_visualizer_fatal, 'size', + 1, + { + 0.0: [2.0], + 0.2: [0.0], + }) + + if node.hitpoints <= 0: + bs.animate(node.bot_radius, 'opacity', { + 0.0: 0.00 + }) + bs.animate( + node.radius_visualizer_circle, 'opacity', { + 0.0: 0.00 + }) + elif babase.app.config.get('bombRadiusVisual'): + + bs.animate_array(node.bot_radius, 'size', 1, { + 0.0: [(node.hitpoints_max - node.hitpoints) * 0.0045], + 0.25: [(node.hitpoints_max - node.hitpoints) * 0.0045] + }) + bs.animate(node.bot_radius, 'opacity', { + 0.0: 0.00, + 0.25: 0.05 + }) + + bs.animate_array(node.radius_visualizer_circle, 'size', 1, { + 0.0: [(node.hitpoints_max - node.hitpoints) * 0.0045], + 0.25: [(node.hitpoints_max - node.hitpoints) * 0.0045] + }) + + bs.animate( + node.radius_visualizer_circle, 'opacity', { + 0.0: 0.00, + 0.25: 0.1 + }) + + if not babase.app.config.get('bombRadiusVisual'): + bs.animate_array(node.bot_radius, 'size', 1, { + 0.0: [0], + 0.25: [0] + }) + bs.animate(node.bot_radius, 'opacity', { + 0.0: 0.00, + 0.25: 0.0 + }) + + bs.animate_array(node.radius_visualizer_circle, 'size', + 1, { + 0.0: [0], + 0.25: [0] + }) + + bs.animate( + node.radius_visualizer_circle, 'opacity', { + 0.0: 0.00, + 0.25: 0.0 + }) + + +Spaz.handlemessage = bot_handlemessage(Spaz.handlemessage) + + +def count_bomb(*args, count, color): + if args[0].node.exists(): + text = bs.newnode('math', owner=args[0].node, + attrs={'input1': (0, 0.7, 0), + 'operation': 'add'}) + args[0].node.connectattr('position', text, 'input2') + args[0].spaztext = bs.newnode('text', + owner=args[0].node, + attrs={ + 'text': count, + 'in_world': True, + 'color': color, + 'shadow': 1.0, + 'flatness': 1.0, + 'scale': 0.012, + 'h_align': 'center', + }) + + args[0].node.connectattr('position', args[0].spaztext, + 'position') + bs.animate(args[0].spaztext, 'scale', + {0: 0, 0.3: 0.03, 0.5: 0.025, 0.8: 0.025, 1.0: 0.0}) + + +def doTestButton(self): + if isinstance(bs.get_foreground_host_session(), MainMenuSession): + bui.screenmessage('Join any map to start using it.', color=(.8, .8, .1)) + return + + activity = bs.get_foreground_host_activity() + if activity is not None: + bui.containerwidget(edit=self._root_widget, transition='out_left') + bs.Call(PracticeWindow()) + else: + bs.screenmessage('Only works on local games.', color=(.8, .8, .1)) + + +# --------------------------------------------------------------- + + +class NewBotSet(SpazBotSet): + + def __init__(self): + """Create a bot-set.""" + + # We spread our bots out over a few lists so we can update + # them in a staggered fashion. + super().__init__() + + def start_moving(self) -> None: + """Start processing bot AI updates so they start doing their thing.""" + self._bot_update_timer = bui.AppTimer( + 0.05, bs.WeakCall(self._update), repeat=True + ) + + def _update(self) -> None: + # Update one of our bot lists each time through. + # First off, remove no-longer-existing bots from the list. + try: + bot_list = self._bot_lists[self._bot_update_list] = ([ + b for b in self._bot_lists[self._bot_update_list] if b + ]) + except Exception: + bot_list = [] + babase.print_exception('Error updating bot list: ' + + str(self._bot_lists[ + self._bot_update_list])) + self._bot_update_list = (self._bot_update_list + + 1) % self._bot_list_count + + # Update our list of player points for the bots to use. + player_pts = [] + for player in bs.getactivity().players: + assert isinstance(player, bs.Player) + try: + # TODO: could use abstracted player.position here so we + # don't have to assume their actor type, but we have no + # abstracted velocity as of yet. + if player.is_alive(): + assert isinstance(player.actor, Spaz) + assert player.actor.node + player_pts.append( + (bs.Vec3(player.actor.node.position), + bs.Vec3( + player.actor.node.velocity))) + except Exception: + babase.print_exception('Error on bot-set _update.') + + for bot in bot_list: + if not babase.app.config.get('stopBots'): + bot.set_player_points(player_pts) + bot.update_ai() + + def clear(self) -> None: + """Immediately clear out any bots in the set.""" + # Don't do this if the activity is shutting down or dead. + activity = bs.getactivity(doraise=False) + if activity is None or activity.expired: + return + + for i, bot_list in enumerate(self._bot_lists): + for bot in bot_list: + bot.handlemessage(bs.DieMessage(immediate=True)) + self._bot_lists[i] = [] + + def spawn_bot( + self, + bot_type: type[SpazBot], + pos: Sequence[float], + spawn_time: float = 3.0, + on_spawn_call: Callable[[SpazBot], Any] | None = None) -> None: + """Spawn a bot from this set.""" + + spawner.Spawner( + pt=pos, + spawn_time=spawn_time, + send_spawn_message=False, + spawn_callback=bs.Call( + self._spawn_bot, bot_type, pos, on_spawn_call + ), + ) + self._spawning_count += 1 + + def _spawn_bot(self, bot_type: type[SpazBot], pos: Sequence[float], + on_spawn_call: Callable[[SpazBot], Any] | None) -> None: + spaz = bot_type().autoretain() + bs.getsound('spawn').play(position=pos) + assert spaz.node + spaz.node.handlemessage('flash') + spaz.node.is_area_of_interest = False + spaz.handlemessage(bs.StandMessage(pos, random.uniform(0, 360))) + self.add_bot(spaz) + self._spawning_count -= 1 + if on_spawn_call is not None: + on_spawn_call(spaz) + + +class DummyBotSet(NewBotSet): + + def _update(self) -> None: + + try: + # Update one of our bot lists each time through. + # First off, remove no-longer-existing bots from the list. + try: + bot_list = self._bot_lists[self._bot_update_list] = ([ + b for b in self._bot_lists[self._bot_update_list] if b + ]) + except Exception: + babase.print_exception('Error updating bot list: ' + + str(self._bot_lists[ + self._bot_update_list])) + self._bot_update_list = (self._bot_update_list + + 1) % self._bot_list_count + + except: + pass + + +class DummyBot(SpazBot): + character = 'Bones' + + def __init__(self): + super().__init__() + if babase.app.config.get('immortalDummy'): + bs.timer(0.2, self.immortal, + repeat=True) + + def immortal(self): + self.hitpoints = self.hitpoints_max = 10000 + try: + bs.emitfx( + position=self.node.position, + count=20, + emit_type='fairydust') + except: + pass + + +class NewChargerBotPro(ChargerBotPro): + default_shields = False + + +# ------------------------------------------------------------------- + +class PracticeTab: + """Defines a tab for use in the gather UI.""" + + def __init__(self, window: PracticeWindow) -> None: + self._window = weakref.ref(window) + + @property + def window(self) -> PracticeWindow: + """The GatherWindow that this tab belongs to.""" + window = self._window() + if window is None: + raise bs.NotFoundError("PracticeTab's window no longer exists.") + return window + + def on_activate( + self, + parent_widget: bs.Widget, + tab_button: bs.Widget, + region_width: float, + region_height: float, + scroll_widget: bs.Widget, + extra_x: float, + ) -> bs.Widget: + """Called when the tab becomes the active one. + + The tab should create and return a container widget covering the + specified region. + """ + raise RuntimeError('Should not get here.') + + def on_deactivate(self) -> None: + """Called when the tab will no longer be the active one.""" + + def save_state(self) -> None: + """Called when the parent window is saving state.""" + + def restore_state(self) -> None: + """Called when the parent window is restoring state.""" + + +def _check_value_change(setting: int, widget: bs.Widget, + value: str) -> None: + bui.textwidget(edit=widget, + text=bs.Lstr(resource='onText') if value else bs.Lstr( + resource='offText')) + + if setting == 0: + if value: + babase.app.config["stopBots"] = True + else: + babase.app.config["stopBots"] = False + elif setting == 1: + if value: + babase.app.config["immortalDummy"] = True + else: + babase.app.config["immortalDummy"] = False + + +class BotsPracticeTab(PracticeTab): + """The about tab in the practice UI""" + + def __init__(self, window: PracticeWindow + ) -> None: + + super().__init__(window) + activity = bs.get_foreground_host_activity() + with activity.context: + try: + if not activity.bot1 or not activity.bot2: + activity.bot1 = DummyBotSet() + activity.bot2 = NewBotSet() + except: + activity.bot1 = DummyBotSet() + activity.bot2 = NewBotSet() + bot_index, count, radius = self.load_settings() + self._container: bs.Widget | None = None + self.count = count + self.radius = radius + self.radius_array = (['Small', 'Medium', 'Big']) + self.parent_widget = None + self.bot1 = activity.bot1 + self.bot2 = activity.bot2 + self.activity = bs.get_foreground_host_activity() + self.image_array = ( + ['bonesIcon', 'neoSpazIcon', 'kronkIcon', 'neoSpazIcon', + 'kronkIcon', + 'zoeIcon', 'ninjaIcon', 'melIcon', 'jackIcon', 'bunnyIcon', + 'neoSpazIcon', 'kronkIcon', 'zoeIcon', 'ninjaIcon', + 'neoSpazIcon', 'kronkIcon', 'zoeIcon', 'ninjaIcon']) + self.bot_array_name = ( + ['Dummy', 'Bomber Lite', 'Brawler Lite', 'Bomber', 'Brawler', + 'Trigger', 'Charger', 'Sticky', + 'Explodey', 'Bouncy', 'Pro Bomber', + 'Pro Brawler', 'Pro Trigger', 'Pro Charger', + 'S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger']) + + self.setting_name = (['Stop Bots', 'Immortal Dummy']) + self.config = (['stopBots', 'immortalDummy']) + + self.bot_array = ( + [DummyBot, BomberBotLite, BrawlerBotLite, SpazBot, BrawlerBot, + TriggerBot, + ChargerBot, StickyBot, ExplodeyBot, BouncyBot, + BomberBotPro, BrawlerBotPro, TriggerBotPro, NewChargerBotPro, + BomberBotProShielded, BrawlerBotProShielded, + TriggerBotProShielded, ChargerBotProShielded]) + + self._icon_index = bot_index + + def on_activate( + self, + parent_widget: bs.Widget, + tab_button: bs.Widget, + region_width: float, + region_height: float, + scroll_widget: bs.Widget, + extra_x: float, + ) -> bui.Widget: + + b_size_2 = 100 + spacing_h = -50 + mask_texture = bui.gettexture('characterIconMask') + spacing_v = 60 + + self.parent_widget = parent_widget + + self._scroll_width = region_width * 0.8 + self._scroll_height = region_height * 0.6 + self._scroll_position = ((region_width - self._scroll_width) * 0.5, + (region_height - self._scroll_height) * 0.5) + + self._sub_width = self._scroll_width + self._sub_height = 200 + + self.container_h = 600 + bots_height = self.container_h - 50 + + self._subcontainer = bui.containerwidget( + parent=scroll_widget, + size=(self._sub_width, self.container_h), + background=False, + selection_loops_to_parent=True) + + bui.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, + bots_height), + size=(0, 0), + color=(1.0, 1.0, 1.0), + scale=1.3, + h_align='center', + v_align='center', + text='Spawn Bot', + maxwidth=200) + + tint1, tint2, color = self.check_color() + + self._bot_button = bot = bui.buttonwidget( + parent=self._subcontainer, + autoselect=True, + position=(self._sub_width * 0.5 - b_size_2 * 0.5, + bots_height + spacing_h * 3), + on_activate_call=self._bot_window, + size=(b_size_2, b_size_2), + label='', + color=color, + tint_texture=(bui.gettexture( + self.image_array[self._icon_index] + 'ColorMask')), + tint_color=tint1, + tint2_color=tint2, + texture=bui.gettexture(self.image_array[self._icon_index]), + mask_texture=mask_texture) + + bui.textwidget( + parent=self._subcontainer, + h_align='center', + v_align='center', + position=(self._sub_width * 0.5, + bots_height + spacing_h * 4 + 10), + size=(0, 0), + draw_controller=bot, + text='Bot Type', + scale=1.0, + color=bui.app.ui_v1.title_color, + maxwidth=130) + + bui.textwidget(parent=self._subcontainer, + position=( + self._sub_width * 0.005, + bots_height + + spacing_h * 7), + size=(100, 30), + text='Count', + h_align='left', + color=(0.8, 0.8, 0.8), + v_align='center', + maxwidth=200) + self.count_text = txt = bui.textwidget(parent=self._subcontainer, + position=( + self._sub_width * 0.85 - spacing_v * 2, + bots_height + + spacing_h * 7), + size=(0, 28), + text=str(self.count), + editable=False, + color=(0.6, 1.0, 0.6), + maxwidth=150, + h_align='center', + v_align='center', + padding=2) + self.button_bot_left = btn1 = bui.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.85 - spacing_v - 14, + bots_height + + spacing_h * 7), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=self.decrease_count, + repeat=True) + self.button_bot_right = btn2 = bui.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.85 - 14, + bots_height + + spacing_h * 7), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=self.increase_count, + repeat=True) + + bui.textwidget(parent=self._subcontainer, + position=( + self._sub_width * 0.005, + bots_height + + spacing_h * 8), + size=(100, 30), + text='Spawn Radius', + h_align='left', + color=(0.8, 0.8, 0.8), + v_align='center', + maxwidth=200) + + self.radius_text = txt = bui.textwidget(parent=self._subcontainer, + position=( + self._sub_width * 0.85 - spacing_v * 2, + bots_height + + spacing_h * 8), + size=(0, 28), + text=self.radius_array[ + self.radius], + editable=False, + color=(0.6, 1.0, 0.6), + maxwidth=50, + h_align='center', + v_align='center', + padding=2) + self.button_bot_left = btn1 = bui.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.85 - spacing_v - 14, + bots_height + + spacing_h * 8), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=self.decrease_radius, + repeat=True) + self.button_bot_right = btn2 = bui.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.85 - 14, + bots_height + + spacing_h * 8), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=self.increase_radius, + repeat=True) + + self.button = bui.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.25 - 40, + bots_height + + spacing_h * 6), + size=(80, 50), + autoselect=True, + button_type='square', + label='Spawn', + on_activate_call=self.do_spawn_bot) + + self.button = bui.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.75 - 40, + bots_height + + spacing_h * 6), + size=(80, 50), + autoselect=True, + button_type='square', + color=(1, 0.2, 0.2), + label='Clear', + on_activate_call=self.clear_bot) + + i = 0 + for name in self.setting_name: + bui.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.005, + bots_height + spacing_h * (9 + i)), + size=(100, 30), + text=name, + h_align='left', + color=(0.8, 0.8, 0.8), + v_align='center', + maxwidth=200) + value = babase.app.config.get(self.config[i]) + txt2 = bui.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.8 - spacing_v / 2, + bots_height + + spacing_h * (9 + i)), + size=(0, 28), + text=bs.Lstr(resource='onText') if value else bs.Lstr( + resource='offText'), + editable=False, + color=(0.6, 1.0, 0.6), + maxwidth=50, + h_align='right', + v_align='center', + padding=2) + bui.checkboxwidget(parent=self._subcontainer, + text='', + position=(self._sub_width * 0.8 - 15, + bots_height + + spacing_h * (9 + i)), + size=(30, 30), + autoselect=False, + textcolor=(0.8, 0.8, 0.8), + value=value, + on_value_change_call=bs.Call( + _check_value_change, + i, txt2)) + i += 1 + + return self._subcontainer + + def _bot_window(self) -> None: + BotPicker( + parent=self.parent_widget, + delegate=self) + + def increase_count(self): + if self.count < 10: + self.count += 1 + + bui.textwidget(edit=self.count_text, + text=str(self.count)) + self.save_settings() + + def decrease_count(self): + if self.count > 1: + self.count -= 1 + + bui.textwidget(edit=self.count_text, + text=str(self.count)) + self.save_settings() + + def increase_radius(self): + if self.radius < 2: + self.radius += 1 + + bui.textwidget(edit=self.radius_text, + text=self.radius_array[self.radius]) + self.save_settings() + + def decrease_radius(self): + if self.radius > 0: + self.radius -= 1 + + bui.textwidget(edit=self.radius_text, + text=self.radius_array[self.radius]) + self.save_settings() + + def clear_bot(self): + bs.screenmessage('Cleared', color=(1, 0.1, 0.1)) + activity = bs.get_foreground_host_activity() + with activity.context: + self.bot1.clear() + self.bot2.clear() + + def do_spawn_bot(self, clid: int = -1) -> None: + bs.screenmessage('Spawned', color=(0.2, 1, 0.2)) + activity = bs.get_foreground_host_activity() + with activity.context: + for i in bs.get_foreground_host_activity().players: + if i.sessionplayer.inputdevice.client_id == clid: + if i.node: + bot_type = self._icon_index + for a in range(self.count): + x = (random.randrange + (-10, 10) / 10) * math.pow(self.radius + 1, 2) + z = (random.randrange + (-10, 10) / 10) * math.pow(self.radius + 1, 2) + pos = (i.node.position[0] + x, + i.node.position[1], + i.node.position[2] + z) + if bot_type == 0: + self.bot1.spawn_bot(self.bot_array[0], + pos=pos, + spawn_time=1.0) + else: + self.bot2.spawn_bot(self.bot_array[bot_type], + pos=pos, + spawn_time=1.0) + break + + def on_bots_picker_pick(self, character: str) -> None: + """A bots has been selected by the picker.""" + if not self.parent_widget: + return + + # The player could have bought a new one while the picker was u + self._icon_index = self.bot_array_name.index( + character) if character in self.bot_array_name else 0 + self._update_character() + + def _update_character(self, change: int = 0) -> None: + if self._bot_button: + tint1, tint2, color = self.check_color() + + bui.buttonwidget( + edit=self._bot_button, + texture=bui.gettexture(self.image_array[self._icon_index]), + tint_texture=(bui.gettexture( + self.image_array[self._icon_index] + 'ColorMask')), + color=color, + tint_color=tint1, + tint2_color=tint2) + self.save_settings() + + def load_settings(self): + try: + if babase.app.config.get("botsSpawnSetting") is None: + babase.app.config["botsSpawnSetting"] = (0, 1, 0) + bot_index, count, radius = babase.app.config.get( + "botsSpawnSetting") + else: + bot_index, count, radius = babase.app.config.get( + "botsSpawnSetting") + except: + babase.app.config["botsSpawnSetting"] = (0, 1, 0) + bot_index, count, radius = babase.app.config.get("botsSpawnSetting") + values = bot_index, count, radius + return values + + def save_settings(self): + babase.app.config["botsSpawnSetting"] = (self._icon_index, self.count, + self.radius) + babase.app.config.commit() + + def check_color(self): + if self.bot_array_name[self._icon_index] in ( + 'Pro Bomber', 'Pro Brawler', + 'Pro Trigger', 'Pro Charger', + 'S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger'): + tint1 = (1.0, 0.2, 0.1) + tint2 = (0.6, 0.1, 0.05) + elif self.bot_array_name[self._icon_index] in 'Bouncy': + tint1 = (1, 1, 1) + tint2 = (1.0, 0.5, 0.5) + elif self.bot_array_name[self._icon_index] in ('Brawler Lite', + 'Bomber Lite'): + tint1 = (1.2, 0.9, 0.2) + tint2 = (1.0, 0.5, 0.6) + else: + tint1 = (0.6, 0.6, 0.6) + tint2 = (0.1, 0.3, 0.1) + + if self.bot_array_name[self._icon_index] in ( + 'S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger'): + color = (1.3, 1.2, 3.0) + else: + color = (1.0, 1.0, 1.0) + + colors = tint1, tint2, color + return colors + + +class PowerUpPracticeTab(PracticeTab): + """The about tab in the practice UI""" + + def __init__(self, window: PracticeWindow) -> None: + super().__init__(window) + self._container: bs.Widget | None = None + self.count = 1 + self.parent_widget = None + self.activity = bs.get_foreground_host_activity() + + self.power_list = (['Bomb', 'Curse', 'Health', 'IceBombs', + 'ImpactBombs', 'LandMines', 'Punch', + 'Shield', 'StickyBombs']) + + self.power_list_type_name = ( + ['Tripple Bombs', 'Curse', 'Health', 'Ice Bombs', + 'Impact Bombs', 'Land Mines', 'Punch', + 'Shield', 'Sticky Bombs']) + + self.power_list_type = ( + ['triple_bombs', 'curse', 'health', 'ice_bombs', + 'impact_bombs', 'land_mines', 'punch', + 'shield', 'sticky_bombs']) + self._icon_index = self.load_settings() + + self.setting_name = ( + ['Bomb Countdown', 'Bomb Radius Visualizer', 'Powerups Expire']) + self.config = (['bombCountdown', 'bombRadiusVisual', 'powerupsExpire']) + + def on_activate( + self, + parent_widget: bs.Widget, + tab_button: bs.Widget, + region_width: float, + region_height: float, + scroll_widget: bs.Widget, + extra_x: float, + ) -> bs.Widget: + + b_size_2 = 100 + spacing_h = -50 + spacing_v = 60 + + self.parent_widget = parent_widget + + self._scroll_width = region_width * 0.8 + self._scroll_height = region_height * 0.6 + self._scroll_position = ((region_width - self._scroll_width) * 0.5, + (region_height - self._scroll_height) * 0.5) + + self._sub_width = self._scroll_width + self._sub_height = 200 + + self.container_h = 550 + power_height = self.container_h - 50 + + self._subcontainer = bui.containerwidget( + parent=scroll_widget, + size=(self._sub_width, self.container_h), + background=False, + selection_loops_to_parent=True) + + bui.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, + power_height), + size=(0, 0), + color=(1.0, 1.0, 1.0), + scale=1.3, + h_align='center', + v_align='center', + text='Spawn Power Up', + maxwidth=200) + + self._power_button = bot = bui.buttonwidget( + parent=self._subcontainer, + autoselect=True, + position=(self._sub_width * 0.5 - b_size_2 * 0.5, + power_height + spacing_h * 3), + on_activate_call=self._power_window, + size=(b_size_2, b_size_2), + label='', + color=(1, 1, 1), + texture=bui.gettexture('powerup' + + self.power_list[ + self._icon_index])) + + bui.textwidget( + parent=self._subcontainer, + h_align='center', + v_align='center', + position=(self._sub_width * 0.5, + power_height + spacing_h * 4 + 10), + size=(0, 0), + draw_controller=bot, + text='Power Up Type', + scale=1.0, + color=bui.app.ui_v1.title_color, + maxwidth=300) + + self.button = bui.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.25 - 40, + power_height + + spacing_h * 6), + size=(80, 50), + autoselect=True, + button_type='square', + label='Spawn', + on_activate_call=self.get_powerup) + + self.button = bui.buttonwidget( + parent=self._subcontainer, + position=( + self._sub_width * 0.75 - 40, + power_height + + spacing_h * 6), + size=(80, 50), + autoselect=True, + button_type='square', + color=(1, 0.2, 0.2), + label='Debuff', + on_activate_call=self.debuff) + + i = 0 + for name in self.setting_name: + bui.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.005, + power_height + spacing_h * (7 + i)), + size=(100, 30), + text=name, + h_align='left', + color=(0.8, 0.8, 0.8), + v_align='center', + maxwidth=200) + value = babase.app.config.get(self.config[i]) + txt2 = bui.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.8 - spacing_v / 2, + power_height + + spacing_h * (7 + i)), + size=(0, 28), + text=bs.Lstr(resource='onText') if value else bs.Lstr( + resource='offText'), + editable=False, + color=(0.6, 1.0, 0.6), + maxwidth=400, + h_align='right', + v_align='center', + padding=2) + bui.checkboxwidget(parent=self._subcontainer, + text='', + position=(self._sub_width * 0.8 - 15, + power_height + + spacing_h * (7 + i)), + size=(30, 30), + autoselect=False, + textcolor=(0.8, 0.8, 0.8), + value=value, + on_value_change_call=bs.Call( + self._check_value_change, + i, txt2)) + i += 1 + + return self._subcontainer + + def debuff(self): + bs.screenmessage('Debuffed', color=(1, 0.1, 0.1)) + activity = bs.get_foreground_host_activity() + with activity.context: + for i in activity.players: + Spaz._gloves_wear_off(i.actor) + Spaz._multi_bomb_wear_off(i.actor) + Spaz._bomb_wear_off(i.actor) + i.actor.node.mini_billboard_1_end_time = 0 + i.actor.node.mini_billboard_2_end_time = 0 + i.actor.node.mini_billboard_3_end_time = 0 + i.actor._multi_bomb_wear_off_flash_timer = None + i.actor._boxing_gloves_wear_off_flash_timer = None + i.actor._bomb_wear_off_flash_timer = None + Spaz.set_land_mine_count(i.actor, min(0, 0)) + i.actor.shield_hitpoints = 1 + + def get_powerup(self, clid: int = -1) -> None: + bs.screenmessage('Spawned', color=(0.2, 1, 0.2)) + activity = bs.get_foreground_host_activity() + with activity.context: + for i in activity.players: + if i.sessionplayer.inputdevice.client_id == clid: + if i.node: + x = (random.choice([-7, 7]) / 10) + z = (random.choice([-7, 7]) / 10) + pos = (i.node.position[0] + x, + i.node.position[1], + i.node.position[2] + z) + PowerupBox(position=pos, + poweruptype=self.power_list_type + [self._icon_index]).autoretain() + + def _power_window(self) -> None: + PowerPicker( + parent=self.parent_widget, + delegate=self) + + def on_power_picker_pick(self, power: str) -> None: + """A power up has been selected by the picker.""" + if not self.parent_widget: + return + + # The player could have bought a new one while the picker was u + self._icon_index = self.power_list.index( + power) if power in self.power_list else 0 + self._update_power() + + def _update_power(self, change: int = 0) -> None: + if self._power_button: + bui.buttonwidget( + edit=self._power_button, + texture=(bui.gettexture('powerup' + + self.power_list[ + self._icon_index]))) + self.save_settings() + + def _check_value_change(self, setting: int, widget: bs.Widget, + value: str) -> None: + bui.textwidget(edit=widget, + text=bs.Lstr(resource='onText') if value else bs.Lstr( + resource='offText')) + + activity = bs.get_foreground_host_activity() + with activity.context: + if setting == 0: + if value: + babase.app.config["bombCountdown"] = True + else: + babase.app.config["bombCountdown"] = False + elif setting == 1: + if value: + babase.app.config["bombRadiusVisual"] = True + else: + babase.app.config["bombRadiusVisual"] = False + activity = bs.get_foreground_host_activity() + # Iterate through all nodes in the activity's scene + for i in self.get_all_bombs(): + update_bomb_visual(i) + for i in activity.players: + update_hit_visual(i.actor) + + elif setting == 2: + if value: + babase.app.config["powerupsExpire"] = True + else: + babase.app.config["powerupsExpire"] = False + + # def get_all_bombs(self) -> list[bs.Node]: + # """Returns all bomb nodes in the current activity.""" + # return [_.getdelegate(object) for _ in bs.getnodes() if _.getnodetype() in ('bomb','prop')] + + import bascenev1 as bs + + def get_all_bombs(self) -> list: + """Return all bomb actors (including TNT) but exclude powerups.""" + bombs = [] + + for node in bs.getnodes(): + if not node.exists(): + continue + # if node.getnodetype() in ('bomb', 'prop'): + # Try to resolve this node as a Bomb delegate + delegate = node.getdelegate(Bomb) + if delegate is not None: + bombs.append(delegate) + return bombs + + def load_settings(self): + try: + if babase.app.config.get("powerSpawnSetting") is None: + babase.app.config["powerSpawnSetting"] = 0 + power_index = babase.app.config.get("powerSpawnSetting") + else: + power_index = babase.app.config.get( + "powerSpawnSetting") + except: + babase.app.config["powerSpawnSetting"] = 0 + power_index = babase.app.config.get("powerSpawnSetting") + values = power_index + return values + + def save_settings(self): + babase.app.config["powerSpawnSetting"] = self._icon_index + babase.app.config.commit() + + +class OthersPracticeTab(PracticeTab): + """The about tab in the practice UI""" + + def __init__(self, window: PracticeWindow) -> None: + super().__init__(window) + self._container: bs.Widget | None = None + self.count = 1 + self.parent_widget = None + self.activity = bs.get_foreground_host_activity() + self.setting_name = (['Pause On Window', 'Invincible', 'Epic Mode']) + self.config = (['pause', 'invincible']) + + def on_activate( + self, + parent_widget: bs.Widget, + tab_button: bs.Widget, + region_width: float, + region_height: float, + scroll_widget: bs.Widget, + extra_x: float, + ) -> bs.Widget: + spacing_v = 60 + spacing_h = -50 + + self.parent_widget = parent_widget + + self._scroll_width = region_width * 0.8 + self._scroll_height = region_height * 0.6 + self._scroll_position = ((region_width - self._scroll_width) * 0.5, + (region_height - self._scroll_height) * 0.5) + + self._sub_width = self._scroll_width + + self.container_h = 300 + other_height = self.container_h - 50 + + self._subcontainer = bui.containerwidget( + parent=scroll_widget, + size=(self._sub_width, self.container_h), + background=False, + selection_loops_to_parent=True) + + bui.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, + other_height), + size=(0, 0), + color=(1.0, 1.0, 1.0), + scale=1.3, + h_align='center', + v_align='center', + text='Others', + maxwidth=200) + + i = 0 + for name in self.setting_name: + bui.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.005, + other_height + spacing_h * (2 + i)), + size=(100, 30), + text=name, + h_align='left', + color=(0.8, 0.8, 0.8), + v_align='center', + maxwidth=200) + if name == 'Epic Mode': + activity = bs.get_foreground_host_activity() + value = activity.globalsnode.slow_motion + else: + value = babase.app.config.get(self.config[i]) + txt2 = bui.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.8 - spacing_v / 2, + other_height + + spacing_h * (2 + i)), + size=(0, 28), + text=bs.Lstr(resource='onText') if value else bs.Lstr( + resource='offText'), + editable=False, + color=(0.6, 1.0, 0.6), + maxwidth=400, + h_align='right', + v_align='center', + padding=2) + bui.checkboxwidget(parent=self._subcontainer, + text='', + position=(self._sub_width * 0.8 - 15, + other_height + + spacing_h * (2 + i)), + size=(30, 30), + autoselect=False, + textcolor=(0.8, 0.8, 0.8), + value=value, + on_value_change_call=bs.Call( + self._check_value_change, + i, txt2)) + i += 1 + + return self._subcontainer + + def _check_value_change(self, setting: int, widget: bs.Widget, + value: str) -> None: + bui.textwidget(edit=widget, + text=bs.Lstr(resource='onText') if value else bs.Lstr( + resource='offText')) + + activity = bs.get_foreground_host_activity() + with activity.context: + if setting == 0: + if value: + babase.app.config["pause"] = True + self.activity.globalsnode.paused = True + else: + babase.app.config["pause"] = False + self.activity.globalsnode.paused = False + elif setting == 1: + if value: + babase.app.config["invincible"] = True + else: + babase.app.config["invincible"] = False + for i in bs.get_foreground_host_activity().players: + try: + if i.node: + if babase.app.config.get("invincible"): + i.actor.node.invincible = True + else: + i.actor.node.invincible = False + except: + pass + elif setting == 2: + activity = bs.get_foreground_host_activity() + if value: + activity.globalsnode.slow_motion = True + else: + activity.globalsnode.slow_motion = False + + +class PracticeWindow(bui.Window): + class TabID(Enum): + """Our available tab types.""" + + BOTS = 'bots' + POWERUP = 'power up' + OTHERS = 'others' + + def __del__(self): + bui.set_party_window_open(True) + self.activity.globalsnode.paused = False + + def __init__(self, + transition: Optional[str] = 'in_right'): + + self.activity = bs.get_foreground_host_activity() + bui.set_party_window_open(False) + if babase.app.config.get("pause"): + self.activity.globalsnode.paused = True + uiscale = bui.app.ui_v1.uiscale + self.pick = 0 + self._width = 500 + + self._height = (578 if uiscale is babase.UIScale.SMALL else + 670 if uiscale is babase.UIScale.MEDIUM else 800) + extra_x = 100 if uiscale is babase.UIScale.SMALL else 0 + self.extra_x = extra_x + + self._transitioning_out = False + + b_size_2 = 100 + + spacing_h = -50 + spacing = -450 + spacing_v = 60 + self.container_h = 500 + v = self._height - 115.0 + v -= spacing_v * 3.0 + + super().__init__(root_widget=bui.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=(1.3 if uiscale is babase.UIScale.SMALL else + 0.97 if uiscale is babase.UIScale.MEDIUM else 0.8), + stack_offset=(0, -10) if uiscale is babase.UIScale.SMALL else ( + 240, 0) if uiscale is babase.UIScale.MEDIUM else (330, 20))) + + self._sub_height = 200 + + self._scroll_width = self._width * 0.8 + self._scroll_height = self._height * 0.6 + self._scroll_position = ((self._width - self._scroll_width) * 0.5, + (self._height - self._scroll_height) * 0.5) + + self._scrollwidget = bui.scrollwidget(parent=self._root_widget, + size=(self._scroll_width, + self._scroll_height), + color=(0.55, 0.55, 0.55), + highlight=False, + position=self._scroll_position) + + bui.containerwidget(edit=self._scrollwidget, + claims_left_right=True) + + # --------------------------------------------------------- + + x_offs = 100 if uiscale is babase.UIScale.SMALL else 0 + + self._current_tab: PracticeWindow.TabID | None = None + extra_top = 20 if uiscale is babase.UIScale.SMALL else 0 + self._r = 'gatherWindow' + + tabdefs: list[tuple[PracticeWindow.TabID, str]] = [ + (self.TabID.BOTS, 'Bots') + ] + if bui.app.plus.get_v1_account_misc_read_val( + 'enablePublicParties', True + ): + tabdefs.append( + ( + self.TabID.POWERUP, + 'Power Ups') + ) + tabdefs.append( + (self.TabID.OTHERS, 'Others') + ) + + condensed = uiscale is not babase.UIScale.LARGE + t_offs_y = ( + 0 if not condensed else 25 if uiscale is babase.UIScale.MEDIUM else 17 + ) + + tab_buffer_h = (320 if condensed else 250) + 2 * x_offs + + self._sub_width = self._width * 0.8 + + # On small UI, push our tabs up closer to the top of the screen to + # save a bit of space. + tabs_top_extra = 42 if condensed else 0 + self._tab_row = TabRow( + self._root_widget, + tabdefs, + pos=( + self._width * 0.5 - self._sub_width * 0.5, + self._height * 0.79), + size=(self._sub_width, 50), + on_select_call=bui.WeakCall(self._set_tab), + ) + + # Now instantiate handlers for these tabs. + tabtypes: dict[PracticeWindow.TabID, type[PracticeTab]] = { + self.TabID.BOTS: BotsPracticeTab, + self.TabID.POWERUP: PowerUpPracticeTab, + self.TabID.OTHERS: OthersPracticeTab, + } + self._tabs: dict[PracticeWindow.TabID, PracticeTab] = {} + for tab_id in self._tab_row.tabs: + tabtype = tabtypes.get(tab_id) + if tabtype is not None: + self._tabs[tab_id] = tabtype(self) + + bui.widget( + edit=self._tab_row.tabs[tabdefs[-1][0]].button, + right_widget=bui.get_special_widget('back_button'), + ) + if uiscale is babase.UIScale.SMALL: + bui.widget( + edit=self._tab_row.tabs[tabdefs[0][0]].button, + left_widget=bui.get_special_widget('back_button'), + ) + + # ----------------------------------------------------------- + + self.back_button = btn = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(self._width * 0.15 - 30, + self._height * 0.95 - 30), + size=(60, 60), + scale=1.1, + label=bui.charstr(bui.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self.close) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + + bui.textwidget(parent=self._root_widget, + position=(self._width * 0.5, + self._height * 0.95), + size=(0, 0), + color=bui.app.ui_v1.title_color, + scale=1.5, + h_align='center', + v_align='center', + text='Practice Tools', + maxwidth=400) + + self.info_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(self._width * 0.8 - 30, + self._height * 0.15 - 30), + on_activate_call=self._info_window, + size=(60, 60), + label='') + + bui.imagewidget( + parent=self._root_widget, + position=(self._width * 0.8 - 25, + self._height * 0.15 - 25), + size=(50, 50), + draw_controller=self.info_button, + texture=bui.gettexture('achievementEmpty'), + color=(1.0, 1.0, 1.0)) + + self._tab_container: bui.Widget | None = None + + self._restore_state() + + # # ------------------------------------------------------- + + def _info_window(self): + InfoWindow( + parent=self._root_widget) + + def _button(self) -> None: + bui.buttonwidget(edit=None, + color=(0.2, 0.4, 0.8)) + + def close(self) -> None: + """Close the window.""" + bui.containerwidget(edit=self._root_widget, transition='out_right') + + def _set_tab(self, tab_id: TabID) -> None: + if self._current_tab is tab_id: + return + prev_tab_id = self._current_tab + self._current_tab = tab_id + + # We wanna preserve our current tab between runs. + cfg = babase.app.config + cfg['Practice Tab'] = tab_id.value + cfg.commit() + + # Update tab colors based on which is selected. + self._tab_row.update_appearance(tab_id) + + if prev_tab_id is not None: + prev_tab = self._tabs.get(prev_tab_id) + if prev_tab is not None: + prev_tab.on_deactivate() + + # Clear up prev container if it hasn't been done. + if self._tab_container: + self._tab_container.delete() + + tab = self._tabs.get(tab_id) + if tab is not None: + self._tab_container = tab.on_activate( + self._root_widget, + self._tab_row.tabs[tab_id].button, + self._width, + self._height, + self._scrollwidget, + self.extra_x, + ) + return + + def _restore_state(self) -> None: + try: + for tab in self._tabs.values(): + tab.restore_state() + + sel: bui.Widget | None + assert bui.app.classic is not None + winstate = bui.app.ui_v1.window_states.get(type(self), {}) + sel_name = winstate.get('sel_name', None) + assert isinstance(sel_name, (str, type(None))) + current_tab = self.TabID.BOTS + gather_tab_val = bui.app.config.get('Practice Tab') + try: + stored_tab = self.TabID(gather_tab_val) + if stored_tab in self._tab_row.tabs: + current_tab = stored_tab + except ValueError: + pass + self._set_tab(current_tab) + if sel_name == 'Back': + sel = self._back_button + elif sel_name == 'TabContainer': + sel = self._tab_container + elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): + try: + sel_tab_id = self.TabID(sel_name.split(':')[-1]) + except ValueError: + sel_tab_id = self.TabID.BOTS + sel = self._tab_row.tabs[sel_tab_id].button + else: + sel = self._tab_row.tabs[current_tab].button + bui.containerwidget(edit=self._root_widget, selected_child=sel) + + except Exception: + logging.exception('Error restoring state for %s.', self) + + +org_begin = bs._activity.Activity.on_begin + + +def new_begin(self): + """Runs when game is began.""" + org_begin(self) + bui.set_party_window_open(True) + + +bs._activity.Activity.on_begin = new_begin + + +class BotPicker(popup.PopupWindow): + """Popup window for selecting bots to spwan.""" + + def __init__(self, + parent: bui.Widget, + position: tuple[float, float] = (0.0, 0.0), + delegate: Any = None, + scale: float | None = None, + offset: tuple[float, float] = (0.0, 0.0), + selected_character: str | None = None): + del parent # unused here + uiscale = bui.app.ui_v1.uiscale + if scale is None: + scale = (1.85 if uiscale is babase.UIScale.SMALL else + 1.65 if uiscale is babase.UIScale.MEDIUM else 1.23) + + self._delegate = delegate + self._transitioning_out = False + + count = 16 + + columns = 3 + rows = int(math.ceil(float(count) / columns)) + + button_width = 100 + button_height = 100 + button_buffer_h = 10 + button_buffer_v = 15 + + self._width = (10 + columns * (button_width + 2 * button_buffer_h) * + (1.0 / 0.95) * (1.0 / 0.8)) + self._height = self._width * (0.8 + if uiscale is babase.UIScale.SMALL else 1.06) + + self._scroll_width = self._width * 0.8 + self._scroll_height = self._height * 0.8 + self._scroll_position = ((self._width - self._scroll_width) * 0.5, + (self._height - self._scroll_height) * 0.5) + + # creates our _root_widget + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=(0.5, 0.5, 0.5), + offset=offset, + focus_position=self._scroll_position, + focus_size=(self._scroll_width, + self._scroll_height)) + + self._scrollwidget = bui.scrollwidget(parent=self.root_widget, + size=(self._scroll_width, + self._scroll_height), + color=(0.55, 0.55, 0.55), + highlight=False, + position=self._scroll_position) + + bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) + + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 5 + rows * (button_height + + 2 * button_buffer_v) + 100 + self._subcontainer = bui.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False) + + mask_texture = bui.gettexture('characterIconMask') + + bot_list = (['bones', 'neoSpaz', 'kronk', 'neoSpaz', 'kronk', 'zoe', + 'ninja', 'mel', 'jack', 'bunny', + 'neoSpaz', 'kronk', 'zoe', + 'ninja', + 'neoSpaz', 'kronk', 'zoe', + 'ninja']) + bot_list_type = ( + ['Dummy', 'Bomber Lite', 'Brawler Lite', 'Bomber', 'Brawler', + 'Trigger', + 'Charger', 'Sticky', 'Explodey', 'Bouncy', + 'Pro Bomber', 'Pro Brawler', 'Pro Trigger', + 'Pro Charger', 'S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger']) + + index = 0 + for y in range(rows): + for x in range(columns): + + if bot_list_type[index] in ('Pro Bomber', 'Pro Brawler', + 'Pro Trigger', 'Pro Charger', + 'S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger'): + tint1 = (1.0, 0.2, 0.1) + tint2 = (0.6, 0.1, 0.05) + elif bot_list_type[index] in 'Bouncy': + tint1 = (1, 1, 1) + tint2 = (1.0, 0.5, 0.5) + elif bot_list_type[index] in ('Brawler Lite', + 'Bomber Lite'): + tint1 = (1.2, 0.9, 0.2) + tint2 = (1.0, 0.5, 0.6) + else: + tint1 = (0.6, 0.6, 0.6) + tint2 = (0.1, 0.3, 0.1) + + if bot_list_type[index] in ('S.Pro Bomber', 'S.Pro Brawler', + 'S.Pro Trigger', 'S.Pro Charger'): + color = (1.3, 1.2, 3.0) + else: + color = (1.0, 1.0, 1.0) + pos = (x * (button_width + 2 * button_buffer_h) + + button_buffer_h, self._sub_height - (y + 1) * + (button_height + 2 * button_buffer_v) + 12) + btn = bui.buttonwidget( + parent=self._subcontainer, + button_type='square', + position=(pos[0], + pos[1]), + size=(button_width, button_height), + autoselect=True, + texture=bui.gettexture(bot_list[index] + 'Icon'), + tint_texture=bui.gettexture( + bot_list[index] + 'IconColorMask'), + mask_texture=mask_texture, + label='', + color=color, + tint_color=tint1, + tint2_color=tint2, + on_activate_call=bui.Call(self._select_character, + character=bot_list_type[index])) + bui.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + + name = bot_list_type[index] + bui.textwidget(parent=self._subcontainer, + text=name, + position=(pos[0] + button_width * 0.5, + pos[1] - 12), + size=(0, 0), + scale=0.5, + maxwidth=button_width, + draw_controller=btn, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8, 0.8)) + + index += 1 + if index >= len(bot_list): + break + if index >= len(bot_list): + break + + def _select_character(self, character: str) -> None: + if self._delegate is not None: + self._delegate.on_bots_picker_pick(character) + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + bui.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + bui.getsound('swish').play() + self._transition_out() + + +class PowerPicker(popup.PopupWindow): + """Popup window for selecting power up.""" + + def __init__(self, + parent: bui.Widget, + position: tuple[float, float] = (0.0, 0.0), + delegate: Any = None, + scale: float | None = None, + offset: tuple[float, float] = (0.0, 0.0), + selected_character: str | None = None): + del parent # unused here + + if scale is None: + uiscale = bui.app.ui_v1.uiscale + scale = (1.85 if uiscale is babase.UIScale.SMALL else + 1.65 if uiscale is babase.UIScale.MEDIUM else 1.23) + + self._delegate = delegate + self._transitioning_out = False + + count = 7 + + columns = 3 + rows = int(math.ceil(float(count) / columns)) + + button_width = 100 + button_height = 100 + button_buffer_h = 10 + button_buffer_v = 15 + + self._width = (10 + columns * (button_width + 2 * button_buffer_h) * + (1.0 / 0.95) * (1.0 / 0.8)) + self._height = self._width * (0.8 + if uiscale is babase.UIScale.SMALL else 1.06) + + self._scroll_width = self._width * 0.8 + self._scroll_height = self._height * 0.8 + self._scroll_position = ((self._width - self._scroll_width) * 0.5, + (self._height - self._scroll_height) * 0.5) + + # creates our _root_widget + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=(0.5, 0.5, 0.5), + offset=offset, + focus_position=self._scroll_position, + focus_size=(self._scroll_width, + self._scroll_height)) + + self._scrollwidget = bui.scrollwidget(parent=self.root_widget, + size=(self._scroll_width, + self._scroll_height), + color=(0.55, 0.55, 0.55), + highlight=False, + position=self._scroll_position) + + bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) + + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 5 + rows * (button_height + + 2 * button_buffer_v) + 100 + self._subcontainer = bui.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False) + + power_list = (['Bomb', 'Curse', 'Health', 'IceBombs', + 'ImpactBombs', 'LandMines', 'Punch', + 'Shield', 'StickyBombs']) + + power_list_type = (['Tripple Bomb', 'Curse', 'Health', 'Ice Bombs', + 'Impact Bombs', 'Land Mines', 'Punch', + 'Shield', 'Sticky Bombs']) + + index = 0 + for y in range(rows): + for x in range(columns): + pos = (x * (button_width + 2 * button_buffer_h) + + button_buffer_h, self._sub_height - (y + 1) * + (button_height + 2 * button_buffer_v) + 12) + btn = bui.buttonwidget( + parent=self._subcontainer, + button_type='square', + position=(pos[0], + pos[1]), + size=(button_width, button_height), + autoselect=True, + texture=bui.gettexture('powerup' + power_list[index]), + label='', + color=(1, 1, 1), + on_activate_call=bui.Call(self._select_power, + power=power_list[index])) + bui.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + + name = power_list_type[index] + bui.textwidget(parent=self._subcontainer, + text=name, + position=(pos[0] + button_width * 0.5, + pos[1] - 12), + size=(0, 0), + scale=0.5, + maxwidth=button_width, + draw_controller=btn, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8, 0.8)) + + index += 1 + if index >= len(power_list): + break + if index >= len(power_list): + break + + def _select_power(self, power: str) -> None: + if self._delegate is not None: + self._delegate.on_power_picker_pick(power) + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + bui.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + bui.getsound('swish').play() + self._transition_out() + + +class InfoWindow(popup.PopupWindow): + """Popup window for Infos.""" + + def __init__(self, + parent: bs.Widget, + position: tuple[float, float] = (0.0, 0.0), + delegate: Any = None, + scale: float | None = None, + offset: tuple[float, float] = (0.0, 0.0), + selected_character: str | None = None): + del parent # unused here + + if scale is None: + uiscale = bui.app.ui_v1.uiscale + scale = (1.85 if uiscale is babase.UIScale.SMALL else + 1.65 if uiscale is babase.UIScale.MEDIUM else 1.23) + + self._delegate = delegate + self._transitioning_out = False + + self._width = 600 + self._height = self._width * (0.6 + if uiscale is babase.UIScale.SMALL else 0.795) + + # creates our _root_widget + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=(0.5, 0.5, 0.5), + offset=offset, + focus_size=(self._width, + self._height)) + + bui.textwidget(parent=self.root_widget, + position=(self._width * 0.5, + self._height * 0.9), + size=(0, 0), + color=bui.app.ui_v1.title_color, + scale=1.3, + h_align='center', + v_align='center', + text='About', + maxwidth=200) + + text = ('Practice Tools Mod\n' + 'Made By Cross Joy\n' + 'version v' + version + '\n' + '\n' + 'Thx to\n' + 'Mikirog for the Bomb radius visualizer mod.\n' + ) + + lines = text.splitlines() + line_height = 16 + scale_t = 0.56 + + voffs = 0 + i = 0 + for line in lines: + i += 1 + if i <= 3: + color = (1.0, 1.0, 1.0, 1.0) + else: + color = (0.4, 1.0, 1.4, 1.0) + + bui.textwidget( + parent=self.root_widget, + padding=4, + color=color, + scale=scale_t, + flatness=1.0, + size=(0, 0), + position=(self._width * 0.5, self._height * 0.8 + voffs), + h_align='center', + v_align='top', + text=line) + voffs -= line_height + + text_spacing = 70 + + self.button_discord = bui.buttonwidget( + parent=self.root_widget, + position=( + self._width * 0.25 - 40, self._height * 0.2 - 40), + size=(80, 80), + autoselect=True, + button_type='square', + color=(0.447, 0.537, 0.854), + label='', + on_activate_call=self._discord) + bui.imagewidget( + parent=self.root_widget, + position=(self._width * 0.25 - 25, + self._height * 0.2 - 25), + size=(50, 50), + draw_controller=self.button_discord, + texture=bui.gettexture('discordLogo'), + color=(5, 5, 5)) + bui.textwidget( + parent=self.root_widget, + position=(self._width * 0.25, + self._height * 0.2 + text_spacing), + size=(0, 0), + scale=0.75, + draw_controller=self.button_discord, + text='Join us. :D', + h_align='center', + v_align='center', + maxwidth=150, + color=(0.447, 0.537, 0.854)) + + self.button_github = bui.buttonwidget( + parent=self.root_widget, + position=( + self._width * 0.5 - 40, self._height * 0.2 - 40), + size=(80, 80), + autoselect=True, + button_type='square', + color=(0.129, 0.122, 0.122), + label='', + on_activate_call=self._github) + bui.imagewidget( + parent=self.root_widget, + position=(self._width * 0.5 - 25, + self._height * 0.2 - 25), + size=(50, 50), + draw_controller=self.button_github, + texture=bui.gettexture('githubLogo'), + color=(1, 1, 1)) + bui.textwidget( + parent=self.root_widget, + position=(self._width * 0.5, + self._height * 0.2 + text_spacing), + size=(0, 0), + scale=0.75, + draw_controller=self.button_github, + text='Found Bugs?', + h_align='center', + v_align='center', + maxwidth=150, + color=(0.129, 0.122, 0.122)) + + self.button_support = bui.buttonwidget( + parent=self.root_widget, + position=( + self._width * 0.75 - 40, self._height * 0.2 - 40), + size=(80, 80), + autoselect=True, + button_type='square', + color=(0.83, 0.69, 0.21), + label='', + on_activate_call=self._support) + bui.imagewidget( + parent=self.root_widget, + position=(self._width * 0.75 - 25, + self._height * 0.2 - 25), + size=(50, 50), + draw_controller=self.button_support, + texture=bui.gettexture('heart'), + color=(1, 1, 1)) + bui.textwidget( + parent=self.root_widget, + position=(self._width * 0.75, + self._height * 0.2 + text_spacing), + size=(0, 0), + scale=0.75, + draw_controller=self.button_support, + text='Support uwu.', + h_align='center', + v_align='center', + maxwidth=150, + color=(0.83, 0.69, 0.21)) + + def _discord(self): + bui.open_url('https://discord.gg/JyBY6haARJ') + + def _github(self): + bui.open_url('https://github.com/CrossJoy/Bombsquad-Modding') + + def _support(self): + bui.open_url('https://www.buymeacoffee.com/CrossJoy') + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + bui.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + bui.getsound('swish').play() + self._transition_out() diff --git a/plugins/utilities/pro_unlocker.py b/plugins/utilities/pro_unlocker.py new file mode 100644 index 000000000..409cdfc7e --- /dev/null +++ b/plugins/utilities/pro_unlocker.py @@ -0,0 +1,20 @@ +# ba_meta require api 8 +import bascenev1 as bs +import _baplus +import babase + + +original_get_purchased = _baplus.get_purchased + + +def get_purchased(item): + if item.startswith('characters.') or item.startswith('icons.'): + return original_get_purchased(item) + return True + + +# ba_meta export plugin +class Unlock(babase.Plugin): + def on_app_running(self): + babase.app.classic.accounts.have_pro = lambda: True + _baplus.get_purchased = get_purchased diff --git a/plugins/utilities/quick_chat.py b/plugins/utilities/quick_chat.py new file mode 100644 index 000000000..8e3a42b13 --- /dev/null +++ b/plugins/utilities/quick_chat.py @@ -0,0 +1,178 @@ +# ba_meta require api 9 + +import babase +import bauiv1 as bui +import bauiv1lib.party +import bascenev1 as bs +import json +import os + +CONFIGS_DIR = os.path.join('.', 'Configs') +if not os.path.exists(CONFIGS_DIR): + os.makedirs(CONFIGS_DIR) + +MSG_PATH = os.path.join(CONFIGS_DIR, 'quick_chat_msgs.json') +DEFAULT_MESSAGES = ['Hi!', 'Let\'s go!', 'GG!', 'Oops!', 'Good luck!', 'Well played!'] + + +def load_messages(): + if not os.path.exists(MSG_PATH): + save_messages(DEFAULT_MESSAGES) # <--- creates JSON file with default msgs + return DEFAULT_MESSAGES + try: + with open(MSG_PATH, 'r') as f: + return json.load(f) + except Exception: + return DEFAULT_MESSAGES + + +def save_messages(msgs): + with open(MSG_PATH, 'w') as f: + json.dump(msgs, f) + + +class QuickChatPartyWindow(bauiv1lib.party.PartyWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._quick_chat_btn = bui.buttonwidget( + parent=self._root_widget, + position=(self._width - 180, self._height - 50), + size=(150, 60), + scale=0.7, + label='Quick Chat', + button_type='square', + color=(0, 0, 0), + textcolor=(1, 1, 1), + on_activate_call=self._open_quick_chat_menu + ) + + def _open_quick_chat_menu(self): + messages = load_messages() + w, h = 400, 300 + + root = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), size=(w, h), transition='in_scale', scale=1.2, color=( + 0, 0, 0), on_outside_click_call=lambda: bui.containerwidget(edit=root, transition='out_scale')) + + self._msg_scroll = bui.scrollwidget( + parent=root, position=(20, 80), size=(360, 180), color=(0, 0, 0)) + self._msg_col = bui.columnwidget(parent=self._msg_scroll, border=2, margin=0) + + for msg in messages: + bui.buttonwidget( + parent=self._msg_col, + size=(350, 40), + label=msg, + textcolor=(1, 1, 1), + color=(0.4, 0.7, 1), + on_activate_call=lambda m=msg: self._send_and_close(m, root) + ) + + bui.buttonwidget( + parent=root, + position=(20, 20), + size=(110, 45), + label='Add', + color=(0.4, 0.7, 1), + textcolor=(1, 1, 1), + on_activate_call=lambda: self._add_message(root) + ) + + bui.buttonwidget( + parent=root, + position=(140, 20), + size=(110, 45), + label='Remove', + color=(0.4, 0.7, 1), + textcolor=(1, 1, 1), + on_activate_call=lambda: self._remove_message(root) + ) + + bui.buttonwidget( + parent=root, + position=(260, 20), + size=(110, 45), + label='Close', + color=(0.4, 0.7, 1), + textcolor=(1, 1, 1), + on_activate_call=lambda: bui.containerwidget(edit=root, transition='out_scale') + ) + + def _send_and_close(self, message: str, root_widget): + bs.chatmessage(message) + bui.containerwidget(edit=root_widget, transition='out_scale') + + def _add_message(self, parent): + def save_new(): + new_msg = bui.textwidget(query=txt).strip() + if new_msg: + msgs = load_messages() + msgs.append(new_msg) + save_messages(msgs) + bui.screenmessage(f'Added: "{new_msg}"', color=(0, 1, 0)) + bui.containerwidget(edit=win, transition='out_scale') + bui.containerwidget(edit=parent, transition='out_scale') + self._open_quick_chat_menu() + + win = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), size=(300, 140), transition='in_scale', + scale=1.2, color=(0, 0, 0), on_outside_click_call=lambda: bui.containerwidget(edit=win, transition='out_scale')) + + bui.textwidget(parent=win, position=(20, 90), size=(260, 30), + text='New Message:', scale=0.9, h_align='left', v_align='center', color=(1, 1, 1)) + + txt = bui.textwidget(parent=win, position=(20, 60), size=(260, 30), + text='', editable=True, maxwidth=200) + + bui.buttonwidget(parent=win, position=(60, 20), size=(80, 30), + label='OK', color=(0.4, 0.7, 1), textcolor=(1, 1, 1), on_activate_call=save_new) + + bui.buttonwidget(parent=win, position=(160, 20), size=(80, 30), + label='Cancel', color=(0.4, 0.7, 1), textcolor=(1, 1, 1), + on_activate_call=lambda: bui.containerwidget(edit=win, transition='out_scale')) + + def _remove_message(self, parent): + msgs = load_messages() + + if not msgs: + bui.screenmessage("No messages to remove.", color=(1, 0, 0)) + return + + h = 50 + len(msgs) * 45 + h = min(h, 300) + win = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), size=(300, h), transition='in_scale', scale=1.2, color=( + 0, 0, 0), on_outside_click_call=lambda: bui.containerwidget(edit=win, transition='out_scale')) + col = bui.columnwidget(parent=win) + + bui.buttonwidget( + parent=col, + label=f'Colse', + size=(260, 40), + textcolor=(1, 1, 1), + color=(1, 0.2, 0.2), + on_activate_call=lambda: bui.containerwidget(edit=win, transition='out_scale') + ) + for msg in msgs: + bui.buttonwidget( + parent=col, + label=f'Delete: {msg}', + size=(260, 40), + textcolor=(1, 1, 1), + color=(0.4, 0.7, 1), + on_activate_call=lambda m=msg: self._confirm_delete(m, win, parent) + ) + + def _confirm_delete(self, msg, win, parent): + msgs = load_messages() + if msg in msgs: + msgs.remove(msg) + save_messages(msgs) + bui.screenmessage(f'Removed: "{msg}"', color=(1, 0.5, 0)) + bui.containerwidget(edit=win, transition='out_scale') + bui.containerwidget(edit=parent, transition='out_scale') + self._open_quick_chat_menu() + + +# ba_meta export babase.Plugin +class ByANES(babase.Plugin): + def on_app_running(self): + bauiv1lib.party.PartyWindow = QuickChatPartyWindow diff --git a/plugins/utilities/quick_custom_game.py b/plugins/utilities/quick_custom_game.py new file mode 100644 index 000000000..9b8031f94 --- /dev/null +++ b/plugins/utilities/quick_custom_game.py @@ -0,0 +1,410 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +from bauiv1lib.play import PlayWindow +from bauiv1lib.playlist.addgame import PlaylistAddGameWindow +from bascenev1._freeforallsession import FreeForAllSession +from bascenev1lib.activity.multiteamjoin import MultiTeamJoinActivity + +if TYPE_CHECKING: + pass + + +lang = bs.app.lang.language + +if lang == "Spanish": + custom_txt = "personalizar..." +else: + custom_txt = "custom..." + + +if "quick_game_button" in babase.app.config: + config = babase.app.config["quick_game_button"] +else: + config = {"selected": None, "config": None} + babase.app.config["quick_game_button"] = config + babase.app.config.commit() + + +def start_game(session: bs.Session, fadeout: bool = True): + def callback(): + if fadeout: + _babase.unlock_all_input() + try: + bs.new_host_session(session) + except Exception: + from bascenev1lib import mainmenu + + babase.print_exception("exception running session", session) + + # Drop back into a main menu session. + bs.new_host_session(mainmenu.MainMenuSession) + + if fadeout: + _babase.fade_screen(False, time=0.25, endcall=callback) + _babase.lock_all_input() + else: + callback() + + +class SimplePlaylist: + def __init__(self, settings: dict, gametype: type[bs.GameActivity]): + self.settings = settings + self.gametype = gametype + + def pull_next(self) -> None: + if "map" not in self.settings["settings"]: + settings = dict(map=self.settings["map"], **self.settings["settings"]) + else: + settings = self.settings["settings"] + return dict(resolved_type=self.gametype, settings=settings) + + +class CustomSession(FreeForAllSession): + def __init__(self, *args, **kwargs): + # pylint: disable=cyclic-import + self.use_teams = False + self._tutorial_activity_instance = None + bs.Session.__init__( + self, + depsets=[], + team_names=None, + team_colors=None, + min_players=1, + max_players=self.get_max_players(), + ) + + self._series_length = 1 + self._ffa_series_length = 1 + + # Which game activity we're on. + self._game_number = 0 + self._playlist = SimplePlaylist(self._config, self._gametype) + config["selected"] = self._gametype.__name__ + config["config"] = self._config + babase.app.config.commit() + + # Get a game on deck ready to go. + self._current_game_spec: Optional[Dict[str, Any]] = None + self._next_game_spec: Dict[str, Any] = self._playlist.pull_next() + self._next_game: Type[bs.GameActivity] = self._next_game_spec["resolved_type"] + + # Go ahead and instantiate the next game we'll + # use so it has lots of time to load. + self._instantiate_next_game() + + # Start in our custom join screen. + self.setactivity(bs.newactivity(MultiTeamJoinActivity)) + + +class SelectGameWindow(PlaylistAddGameWindow): + def __init__(self, transition: str = "in_right"): + class EditController: + _sessiontype = bs.FreeForAllSession + + def get_session_type(self) -> Type[bs.Session]: + return self._sessiontype + + self._editcontroller = EditController() + self._r = "addGameWindow" + uiscale = bui.app.ui_v1.uiscale + self._width = 750 if uiscale is babase.UIScale.SMALL else 650 + x_inset = 50 if uiscale is babase.UIScale.SMALL else 0 + self._height = ( + 346 + if uiscale is babase.UIScale.SMALL + else 380 + if uiscale is babase.UIScale.MEDIUM + else 440 + ) + top_extra = 30 if uiscale is babase.UIScale.SMALL else 20 + self._scroll_width = 210 + + self._root_widget = bui.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + scale=( + 2.17 + if uiscale is babase.UIScale.SMALL + else 1.5 + if uiscale is babase.UIScale.MEDIUM + else 1.0 + ), + stack_offset=(0, 1) if uiscale is babase.UIScale.SMALL else (0, 0), + ) + + self._back_button = bui.buttonwidget( + parent=self._root_widget, + position=(58 + x_inset, self._height - 53), + size=(165, 70), + scale=0.75, + text_scale=1.2, + label=babase.Lstr(resource="backText"), + autoselect=True, + button_type="back", + on_activate_call=self._back, + ) + self._select_button = select_button = bui.buttonwidget( + parent=self._root_widget, + position=(self._width - (172 + x_inset), self._height - 50), + autoselect=True, + size=(160, 60), + scale=0.75, + text_scale=1.2, + label=babase.Lstr(resource="selectText"), + on_activate_call=self._add, + ) + + if bui.app.ui_v1.use_toolbars: + bui.widget( + edit=select_button, right_widget=bui.get_special_widget("party_button") + ) + + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 28), + size=(0, 0), + scale=1.0, + text=babase.Lstr(resource=self._r + ".titleText"), + h_align="center", + color=bui.app.ui_v1.title_color, + maxwidth=250, + v_align="center", + ) + v = self._height - 64 + + self._selected_title_text = bui.textwidget( + parent=self._root_widget, + position=(x_inset + self._scroll_width + 50 + 30, v - 15), + size=(0, 0), + scale=1.0, + color=(0.7, 1.0, 0.7, 1.0), + maxwidth=self._width - self._scroll_width - 150 - x_inset * 2, + h_align="left", + v_align="center", + ) + v -= 30 + + self._selected_description_text = bui.textwidget( + parent=self._root_widget, + position=(x_inset + self._scroll_width + 50 + 30, v), + size=(0, 0), + scale=0.7, + color=(0.5, 0.8, 0.5, 1.0), + maxwidth=self._width - self._scroll_width - 150 - x_inset * 2, + h_align="left", + ) + + scroll_height = self._height - 100 + + v = self._height - 60 + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=(x_inset + 61, v - scroll_height), + size=(self._scroll_width, scroll_height), + highlight=False, + ) + bui.widget( + edit=self._scrollwidget, + up_widget=self._back_button, + left_widget=self._back_button, + right_widget=select_button, + ) + self._column: Optional[bui.Widget] = None + + v -= 35 + bui.containerwidget( + edit=self._root_widget, + cancel_button=self._back_button, + start_button=select_button, + ) + self._selected_game_type: Optional[Type[bs.GameActivity]] = None + + bui.containerwidget(edit=self._root_widget, selected_child=self._scrollwidget) + + self._game_types: list[type[bs.GameActivity]] = [] + + # Get actual games loading in the bg. + babase.app.meta.load_exported_classes( + bs.GameActivity, self._on_game_types_loaded, completion_cb_in_bg_thread=True + ) + + # Refresh with our initial empty list. We'll refresh again once + # game loading is complete. + self._refresh() + + if config["selected"]: + for gt in self._game_types: + if gt.__name__ == config["selected"]: + self._refresh(selected=gt) + self._set_selected_game_type(gt) + + def _refresh( + self, select_get_more_games_button: bool = False, selected: bool = None + ) -> None: + # from babase.internal import get_game_types + + if self._column is not None: + self._column.delete() + + self._column = bui.columnwidget(parent=self._scrollwidget, border=2, margin=0) + + for i, gametype in enumerate(self._game_types): + + def _doit() -> None: + if self._select_button: + bs.apptimer(0.1, self._select_button.activate) + + txt = bui.textwidget( + parent=self._column, + position=(0, 0), + size=(self._width - 88, 24), + text=gametype.get_display_string(), + h_align="left", + v_align="center", + color=(0.8, 0.8, 0.8, 1.0), + maxwidth=self._scroll_width * 0.8, + on_select_call=babase.Call(self._set_selected_game_type, gametype), + always_highlight=True, + selectable=True, + on_activate_call=_doit, + ) + if i == 0: + bui.widget(edit=txt, up_widget=self._back_button) + + self._get_more_games_button = bui.buttonwidget( + parent=self._column, + autoselect=True, + label=babase.Lstr(resource=self._r + ".getMoreGamesText"), + color=(0.54, 0.52, 0.67), + textcolor=(0.7, 0.65, 0.7), + on_activate_call=self._on_get_more_games_press, + size=(178, 50), + ) + if select_get_more_games_button: + bui.containerwidget( + edit=self._column, + selected_child=self._get_more_games_button, + visible_child=self._get_more_games_button, + ) + + def _add(self) -> None: + _babase.lock_all_input() # Make sure no more commands happen. + bs.apptimer(0.1, _babase.unlock_all_input) + gameconfig = {} + if config["selected"] == self._selected_game_type.__name__: + if config["config"]: + gameconfig = config["config"] + if "map" in gameconfig: + gameconfig["settings"]["map"] = gameconfig.pop("map") + self._selected_game_type.create_settings_ui( + self._editcontroller.get_session_type(), gameconfig, self._edit_game_done + ) + + def _edit_game_done(self, config: Optional[Dict[str, Any]]) -> None: + if config: + CustomSession._config = config + CustomSession._gametype = self._selected_game_type + start_game(CustomSession) + else: + bui.app.ui_v1.clear_main_menu_window(transition="out_right") + bui.app.ui_v1.set_main_menu_window( + SelectGameWindow(transition="in_left").get_root_widget(), + from_window=None, + ) + + def _back(self) -> None: + if not self._root_widget or self._root_widget.transitioning_out: + return + + bui.containerwidget(edit=self._root_widget, transition="out_right") + bui.app.ui_v1.set_main_menu_window( + PlayWindow(transition="in_left").get_root_widget(), + from_window=self._root_widget, + ) + + +PlayWindow._old_init = PlayWindow.__init__ + + +def __init__(self, *args, **kwargs): + self._old_init() + + width = 800 + height = 550 + + def do_quick_game() -> None: + if not self._root_widget or self._root_widget.transitioning_out: + return + + self._save_state() + bui.containerwidget(edit=self._root_widget, transition="out_left") + bui.app.ui_v1.set_main_menu_window( + SelectGameWindow().get_root_widget(), from_window=self._root_widget + ) + + self._quick_game_button = bui.buttonwidget( + parent=self._root_widget, + position=(width - 55 - 120, height - 132), + autoselect=True, + size=(120, 60), + scale=1.1, + text_scale=1.2, + label=custom_txt, + on_activate_call=do_quick_game, + color=(0.54, 0.52, 0.67), + textcolor=(0.7, 0.65, 0.7), + ) + + self._restore_state() + + +def states(self) -> None: + return { + "Team Games": self._teams_button, + "Co-op Games": self._coop_button, + "Free-for-All Games": self._free_for_all_button, + "Back": self._back_button, + "Quick Game": self._quick_game_button, + } + + +def _save_state(self) -> None: + swapped = {v: k for k, v in states(self).items()} + if self._root_widget.get_selected_child() in swapped: + bui.app.ui_v1.window_states[self.__class__.__name__] = swapped[ + self._root_widget.get_selected_child() + ] + else: + babase.print_exception(f"Error saving state for {self}.") + + +def _restore_state(self) -> None: + if not hasattr(self, "_quick_game_button"): + return # ensure that our monkey patched init ran + if self.__class__.__name__ not in bui.app.ui_v1.window_states: + bui.containerwidget(edit=self._root_widget, selected_child=self._coop_button) + return + sel = states(self).get(bui.app.ui_v1.window_states[self.__class__.__name__], None) + if sel: + bui.containerwidget(edit=self._root_widget, selected_child=sel) + else: + bui.containerwidget(edit=self._root_widget, selected_child=self._coop_button) + babase.print_exception(f"Error restoring state for {self}.") + + +# ba_meta export plugin +class QuickGamePlugin(babase.Plugin): + PlayWindow.__init__ = __init__ + PlayWindow._save_state = _save_state + PlayWindow._restore_state = _restore_state diff --git a/plugins/utilities/quickturn.py b/plugins/utilities/quickturn.py new file mode 100644 index 000000000..8bb6e72aa --- /dev/null +++ b/plugins/utilities/quickturn.py @@ -0,0 +1,141 @@ +""" + Quickturn by TheMikirog + + Super Smash Bros Melee's wavedash mechanic ported and tuned for BombSquad. + + Sharp turns while running (releasing run button, changing direction, holding run again) are much faster with this mod, allowing for more dynamic, aggressive play. + Version 3 + + No Rights Reserved +""" + +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +import math +import bascenev1lib + +if TYPE_CHECKING: + pass + +# ba_meta export babase.Plugin + + +class Quickturn(babase.Plugin): + + class FootConnectMessage: + """Spaz started touching the ground""" + + class FootDisconnectMessage: + """Spaz stopped touching the ground""" + + def wavedash(self) -> None: + if not self.node: + return + + isMoving = abs(self.node.move_up_down) >= 0.5 or abs(self.node.move_left_right) >= 0.5 + + if self._dead or not self.grounded or not isMoving: + return + + if self.node.knockout > 0.0 or self.frozen or self.node.hold_node: + return + + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + + if t_ms - self.last_wavedash_time_ms >= self._wavedash_cooldown: + + move = [self.node.move_left_right, -self.node.move_up_down] + vel = [self.node.velocity[0], self.node.velocity[2]] + + move_length = math.hypot(move[0], move[1]) + vel_length = math.hypot(vel[0], vel[1]) + if vel_length < 0.6: + return + move_norm = [m/move_length for m in move] + vel_norm = [v/vel_length for v in vel] + dot = sum(x*y for x, y in zip(move_norm, vel_norm)) + turn_power = min(round(math.acos(dot) / math.pi, 2)*1.3, 1) + + # https://easings.net/#easeInOutQuart + if turn_power < 0.55: + turn_power = 8 * turn_power * turn_power * turn_power * turn_power + else: + turn_power = 0.55 - pow(-2 * turn_power + 2, 4) / 2 + if turn_power < 0.1: + return + + boost_power = math.sqrt(math.pow(vel[0], 2) + math.pow(vel[1], 2)) * 8 + boost_power = min(pow(boost_power, 8), 160) + + self.last_wavedash_time_ms = t_ms + + # FX + bs.emitfx(position=self.node.position, + velocity=(vel[0]*0.5, -1, vel[1]*0.5), + chunk_type='spark', + count=5, + scale=boost_power / 160 * turn_power, + spread=0.25) + + # Boost itself + pos = self.node.position + for i in range(6): + self.node.handlemessage('impulse', pos[0], 0.2+pos[1]+i*0.1, pos[2], + 0, 0, 0, + boost_power * turn_power, + boost_power * turn_power, 0, 0, + move[0], 0, move[1]) + + def new_spaz_init(func): + def wrapper(*args, **kwargs): + + func(*args, **kwargs) + + # args[0] = self + args[0]._wavedash_cooldown = 170 + args[0].last_wavedash_time_ms = -9999 + args[0].grounded = 0 + + return wrapper + bascenev1lib.actor.spaz.Spaz.__init__ = new_spaz_init(bascenev1lib.actor.spaz.Spaz.__init__) + + def new_factory(func): + def wrapper(*args, **kwargs): + func(*args, **kwargs) + + args[0].roller_material.add_actions( + conditions=('they_have_material', + bascenev1lib.gameutils.SharedObjects.get().footing_material), + actions=(('message', 'our_node', 'at_connect', Quickturn.FootConnectMessage), + ('message', 'our_node', 'at_disconnect', Quickturn.FootDisconnectMessage))) + return wrapper + bascenev1lib.actor.spazfactory.SpazFactory.__init__ = new_factory( + bascenev1lib.actor.spazfactory.SpazFactory.__init__) + + def new_handlemessage(func): + def wrapper(*args, **kwargs): + if args[1] == Quickturn.FootConnectMessage: + args[0].grounded += 1 + elif args[1] == Quickturn.FootDisconnectMessage: + if args[0].grounded > 0: + args[0].grounded -= 1 + + func(*args, **kwargs) + return wrapper + bascenev1lib.actor.spaz.Spaz.handlemessage = new_handlemessage( + bascenev1lib.actor.spaz.Spaz.handlemessage) + + def new_on_run(func): + def wrapper(*args, **kwargs): + if args[0]._last_run_value < args[1] and args[1] > 0.8: + Quickturn.wavedash(args[0]) + func(*args, **kwargs) + return wrapper + bascenev1lib.actor.spaz.Spaz.on_run = new_on_run(bascenev1lib.actor.spaz.Spaz.on_run) diff --git a/plugins/utilities/ragdoll_b_gone.py b/plugins/utilities/ragdoll_b_gone.py new file mode 100644 index 000000000..b5d4fd5c7 --- /dev/null +++ b/plugins/utilities/ragdoll_b_gone.py @@ -0,0 +1,123 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 9 + +""" + Ragdoll-B-Gone by TheMikirog + + Removes ragdolls. + Thanos snaps those pesky feet-tripping body sacks out of existence. + Literally that's it. + + Heavily commented for easy modding learning! + + No Rights Reserved + +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +# Let's import everything we need and nothing more. +import babase +import bauiv1 as bui +import bascenev1 as bs +import bascenev1lib +import random +from bascenev1lib.actor.spaz import Spaz +from bascenev1lib.actor.spazfactory import SpazFactory + +if TYPE_CHECKING: + pass + +# ba_meta export babase.Plugin + + +class RagdollBGone(babase.Plugin): + + # We use a decorator to add extra code to existing code, increasing mod compatibility. + # Any gameplay altering mod should master the decorator! + # Here I'm defining a new handlemessage function that'll be replaced. + def new_handlemessage(func): + # This function will return our wrapper function, which is going to take the original function's base arguments. + # Yes, in Python functions are objects that can be passed as arguments. It's bonkers. + # arg[0] is "self", args[1] is "msg" in our original handlemessage function. + # We're working kind of blindly here, so it's good to have the original function + # open in a second window for argument reference. + def wrapper(*args, **kwargs): + if isinstance(args[1], bs.DieMessage): # Replace Spaz death behavior + + # Here we play the gamey death noise in Co-op. + if not args[1].immediate: + if args[0].play_big_death_sound and not args[0]._dead: + SpazFactory.get().single_player_death_sound.play() + + # If our Spaz dies by falling out of the map, we want to keep the ragdoll. + # Ragdolls don't impact gameplay if Spaz dies this way, so it's fine if we leave the behavior as is. + if args[1].how == bs.DeathType.FALL: + # The next two properties are all built-in, so their behavior can't be edited directly without touching the C++ layer. + # We can change their values though! + # "hurt" property is basically the health bar above the player and the blinking when low on health. + # 1.0 means empty health bar and the fastest blinking in the west. + args[0].node.hurt = 1.0 + # Make our Spaz close their eyes permanently and then make their body disintegrate. + # Again, this behavior is built in. We can only trigger it by setting "dead" to True. + args[0].node.dead = True + # After the death animation ends (which is around 2 seconds) let's remove the Spaz our of existence. + bs.timer(2.0, args[0].node.delete) + else: + # Here's our new behavior! + # The idea is to remove the Spaz node and make some sparks for extra flair. + # First we see if that node even exists, just in case. + if args[0].node: + # Make sure Spaz isn't dead, so we can perform the removal. + if not args[0]._dead: + # Run this next piece of code 4 times. + # "i" will start at 0 and becomes higher each iteration until it reaches 3. + for i in range(4): + # XYZ position of our sparks, we'll take the Spaz position as a base. + pos = (args[0].node.position[0], + # Let's spread the sparks across the body, assuming Spaz is standing straight. + # We're gonna change the Y axis position, which is height. + args[0].node.position[1] + i * 0.2, + args[0].node.position[2]) + # This function allows us to spawn particles like sparks and bomb shrapnel. + # We're gonna use sparks here. + bs.emitfx(position=pos, # Here we place our edited position. + velocity=args[0].node.velocity, + # Random amount of sparks between 2 and 5 + count=random.randrange(2, 5), + scale=3.0, + spread=0.2, + chunk_type='spark') + + # Make a Spaz death noise if we're not gibbed. + if not args[0].shattered: + # Get our Spaz's death noises, these change depending on character skins + death_sounds = args[0].node.death_sounds + # Pick a random death noise + sound = death_sounds[random.randrange(len(death_sounds))] + # Play the sound where our Spaz is + bs.Sound.play(sound, position=args[0].node.position) + # Delete our Spaz node immediately. + # Removing stuff is weird and prone to errors, so we're gonna delay it. + bs.timer(0.001, args[0].node.delete) + + # Let's mark our Spaz as dead, so he can't die again. + # Notice how we're targeting the Spaz and not it's node. + # "self.node" is a visual representation of the character while "self" is his game logic. + args[0]._dead = True + # Set his health to zero. This value is independent from the health bar above his head. + args[0].hitpoints = 0 + return + + # Worry no longer! We're not gonna remove all the base game code! + # Here's where we bring it all back. + # If I wanted to add extra code at the end of the base game's behavior, I would just put that at the beginning of my function. + func(*args, **kwargs) + return wrapper + + # Finally we """travel through the game files""" to replace the function we want with our own version. + # We transplant the old function's arguments into our version. + bascenev1lib.actor.spaz.Spaz.handlemessage = new_handlemessage( + bascenev1lib.actor.spaz.Spaz.handlemessage) diff --git a/plugins/utilities/random_join.py b/plugins/utilities/random_join.py new file mode 100644 index 000000000..02caabf18 --- /dev/null +++ b/plugins/utilities/random_join.py @@ -0,0 +1,344 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +import _babase +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bauiv1lib.gather.publictab import PublicGatherTab, PartyEntry, PingThread +if TYPE_CHECKING: + from typing import Callable + +ClassType = TypeVar('ClassType') +MethodType = TypeVar('MethodType') + + +def override(cls: ClassType) -> Callable[[MethodType], MethodType]: + def decorator(newfunc: MethodType) -> MethodType: + funcname = newfunc.__code__.co_name + if hasattr(cls, funcname): + oldfunc = getattr(cls, funcname) + setattr(cls, f'_old_{funcname}', oldfunc) + + setattr(cls, funcname, newfunc) + return newfunc + + return decorator + +# Can this stuff break mro? (P.S. yes, so we're not using super() anymore). +# Although it gives nice auto-completion. +# And anyways, why not just GatherPublicTab = NewGatherPublicTab? +# But hmm, if we imagine someone used `from blah.blah import Blah`, using +# `blah.Blah = NewBlah` AFTERWARDS would be meaningless. + + +class NewPublicGatherTab(PublicGatherTab, PingThread): + + @override(PublicGatherTab) + def _build_join_tab(self, region_width: float, + region_height: float, + oldfunc: Callable = None) -> None: + # noinspection PyUnresolvedReferences + self._old__build_join_tab(region_width, region_height) + + # Copy-pasted from original function. + c_width = region_width + c_height = region_height - 20 + sub_scroll_height = c_height - 125 + sub_scroll_width = 830 + v = c_height - 35 + v -= 60 + + self._random_join_button = bui.buttonwidget( + parent=self._container, + label='random', + size=(90, 45), + position=(710, v + 10), + on_activate_call=bs.WeakCall(self._join_random_server), + ) + bui.widget(edit=self._random_join_button, up_widget=self._host_text, + left_widget=self._filter_text) + + # We could place it somewhere under plugin settings which is kind of + # official way to customise plugins. Although it's too deep: + # Gather Window -> Main Menu -> Settings -> Advanced -(scroll)-> + # Plugins -(scroll probably)-> RandomJoin Settings. + self._random_join_settings_button = bui.buttonwidget( + parent=self._container, + icon=bui.gettexture('settingsIcon'), + size=(40, 40), + position=(820, v + 13), + on_activate_call=bs.WeakCall(self._show_random_join_settings), + ) + + @override(PublicGatherTab) + def _show_random_join_settings(self) -> None: + RandomJoinSettingsPopup( + origin_widget=self._random_join_settings_button) + + @override(PublicGatherTab) + def _get_parties_list(self) -> list[PartyEntry]: + if (self._parties_sorted and + (randomjoin.maximum_ping == 9999 or + # Ensure that we've pinged at least 10%. + len([p for k, p in self._parties_sorted + if p.ping is not None]) > len(self._parties_sorted) / 10)): + randomjoin.cached_parties = [p for k, p in self._parties_sorted] + return randomjoin.cached_parties + + @override(PublicGatherTab) + def _join_random_server(self) -> None: + name_prefixes = set() + parties = [p for p in self._get_parties_list() if + (randomjoin.party_name_contains in p.name + and p.size >= randomjoin.minimum_players + and p.size < p.size_max and (randomjoin.maximum_ping == 9999 + or (p.ping is not None + and p.ping <= randomjoin.maximum_ping)))] + + if not parties: + bui.screenmessage('No suitable servers found; wait', + color=(1, 0, 0)) + bui.getsound('error').play() + return + + for party in parties: + name_prefixes.add(party.name[:6]) + + random.choice(list(name_prefixes)) + + party = random.choice( + [p for p in parties if p.name[:6] in name_prefixes]) + + bs.connect_to_party(party.address, party.port) + + +class RandomJoinSettingsPopup(bui.Window): + def __init__(self, origin_widget: bui.Widget) -> None: + c_width = 600 + c_height = 400 + uiscale = bui.app.ui_v1.uiscale + super().__init__(root_widget=bui.containerwidget( + scale=( + 1.8 + if uiscale is babase.UIScale.SMALL + else 1.55 + if uiscale is babase.UIScale.MEDIUM + else 1.0 + ), + scale_origin_stack_offset=origin_widget.get_screen_space_center(), + stack_offset=(0, -10) + if uiscale is babase.UIScale.SMALL + else (0, 15) + if uiscale is babase.UIScale.MEDIUM + else (0, 0), + size=(c_width, c_height), + transition='in_scale', + )) + + bui.textwidget( + parent=self._root_widget, + size=(0, 0), + h_align='center', + v_align='center', + text='Random Join Settings', + scale=1.5, + color=(0.6, 1.0, 0.6), + maxwidth=c_width * 0.8, + position=(c_width * 0.5, c_height - 60), + ) + + v = c_height - 120 + bui.textwidget( + parent=self._root_widget, + size=(0, 0), + h_align='right', + v_align='center', + text='Maximum ping', + maxwidth=c_width * 0.3, + position=(c_width * 0.4, v), + ) + self._maximum_ping_edit = bui.textwidget( + parent=self._root_widget, + size=(c_width * 0.3, 40), + h_align='left', + v_align='center', + text=str(randomjoin.maximum_ping), + editable=True, + description='Maximum ping (ms)', + position=(c_width * 0.6, v - 20), + autoselect=True, + max_chars=4, + ) + v -= 60 + bui.textwidget( + parent=self._root_widget, + size=(0, 0), + h_align='right', + v_align='center', + text='Minimum players', + maxwidth=c_width * 0.3, + position=(c_width * 0.4, v), + ) + self._minimum_players_edit = bui.textwidget( + parent=self._root_widget, + size=(c_width * 0.3, 40), + h_align='left', + v_align='center', + text=str(randomjoin.minimum_players), + editable=True, + description='Minimum number of players', + position=(c_width * 0.6, v - 20), + autoselect=True, + max_chars=4, + ) + v -= 60 + bui.textwidget( + parent=self._root_widget, + size=(0, 0), + h_align='right', + v_align='center', + text='Party name contains', + maxwidth=c_width * 0.3, + position=(c_width * 0.4, v), + ) + self._party_name_contains_edit = bui.textwidget( + parent=self._root_widget, + size=(c_width * 0.3, 40), + h_align='left', + v_align='center', + text=str(randomjoin.party_name_contains), + editable=True, + description='Party name contains your input', + position=(c_width * 0.6, v - 20), + autoselect=True, + ) + + # Cancel button. + self.cancel_button = btn = bui.buttonwidget( + parent=self._root_widget, + label=babase.Lstr(resource='cancelText'), + size=(180, 60), + color=(1.0, 0.2, 0.2), + position=(40, 30), + on_activate_call=self._cancel, + autoselect=True, + ) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + + # Save button. + self.savebtn = btn = bui.buttonwidget( + parent=self._root_widget, + label=babase.Lstr(resource='saveText'), + size=(180, 60), + position=(c_width - 200, 30), + on_activate_call=self._save, + autoselect=True, + ) + bui.containerwidget(edit=self._root_widget, start_button=btn) + + def _save(self) -> None: + errored = False + minimum_players: int | None = None + maximum_ping: int | None = None + try: + minimum_players = int( + bui.textwidget(query=self._minimum_players_edit)) + except ValueError: + bui.screenmessage('"Minimum players" should be integer', + color=(1, 0, 0)) + bui.getsound('error').play() + errored = True + try: + maximum_ping = int( + bui.textwidget(query=self._maximum_ping_edit)) + except ValueError: + bui.screenmessage('"Maximum ping" should be integer', + color=(1, 0, 0)) + bui.getsound('error').play() + errored = True + party_name_contains = bui.textwidget(query=self._party_name_contains_edit) + if errored: + return + + assert minimum_players is not None + assert maximum_ping is not None + + if minimum_players < 0: + bui.screenmessage('"Minimum players" should be at least 0', + color=(1, 0, 0)) + bui.getsound('error').play() + errored = True + + if maximum_ping <= 0: + bui.screenmessage('"Maximum ping" should be greater than 0', + color=(1, 0, 0)) + bui.getsound('error').play() + bui.screenmessage('(use 9999 as dont-care value)', + color=(1, 0, 0)) + errored = True + + if errored: + return + + randomjoin.maximum_ping = maximum_ping + randomjoin.minimum_players = minimum_players + randomjoin.party_name_contains = party_name_contains + + randomjoin.commit_config() + bui.getsound('shieldUp').play() + self._transition_out() + + def _cancel(self) -> None: + bui.getsound('shieldDown').play() + self._transition_out() + + def _transition_out(self) -> None: + bui.containerwidget(edit=self._root_widget, transition='out_scale') + + +class RandomJoin: + def __init__(self) -> None: + self.cached_parties: list[PartyEntry] = [] + self.maximum_ping: int = 9999 + self.minimum_players: int = 2 + self.party_name_contains: str = 'FFA @' + self.load_config() + + def load_config(self) -> None: + cfg = babase.app.config.get('Random Join', { + 'maximum_ping': self.maximum_ping, + 'minimum_players': self.minimum_players, + 'party_name_contains': self.party_name_contains, + }) + try: + self.maximum_ping = cfg['maximum_ping'] + self.minimum_players = cfg['minimum_players'] + self.party_name_contains = cfg['party_name_contains'] + except KeyError: + bui.screenmessage('Error: RandomJoin config is broken, resetting..', + color=(1, 0, 0), log=True) + bui.getsound('error').play() + self.commit_config() + + def commit_config(self) -> None: + babase.app.config['Random Join'] = { + 'maximum_ping': self.maximum_ping, + 'minimum_players': self.minimum_players, + 'party_name_contains': self.party_name_contains, + } + babase.app.config.commit() + + +randomjoin = RandomJoin() + + +# ba_meta require api 9 +# ba_meta export babase.Plugin +class RandomJoinPlugin(babase.Plugin): + def on_app_running(self) -> None: + # I feel bad that all patching logic happens not here. + pass diff --git a/plugins/utilities/random_play.py b/plugins/utilities/random_play.py new file mode 100644 index 000000000..f3d996478 --- /dev/null +++ b/plugins/utilities/random_play.py @@ -0,0 +1,207 @@ +# ba_meta require api 9 +from random import choice, randint +from typing import Any, Union + +# pylint: disable=import-error +import babase +from bascenev1 import ( + Session, + MultiTeamSession, + FreeForAllSession, + DualTeamSession, + GameActivity, + newactivity, + new_host_session, +) + +from bauiv1 import Widget, UIScale, buttonwidget +from bauiv1lib.play import PlaylistSelectContext +from bauiv1lib.playlist.browser import PlaylistBrowserWindow +from bascenev1lib.activity.multiteamjoin import MultiTeamJoinActivity + +DEFAULT_TEAM_COLORS = ((0.1, 0.25, 1.0), (1.0, 0.25, 0.2)) +DEFAULT_TEAM_NAMES = ("Blue", "Red") + + +# More or less copied from game code +# I have no idea what I'm doing here +class RandomPlaySessionMixin(MultiTeamSession, Session): + def __init__(self, playlist) -> None: + """Set up playlists & launch a bascenev1.Activity to accept joiners.""" + + app = babase.app + classic = app.classic + assert classic is not None + _cfg = app.config + + super(MultiTeamSession, self).__init__( + [], + team_names=DEFAULT_TEAM_NAMES, + team_colors=DEFAULT_TEAM_COLORS, + min_players=1, + max_players=self.get_max_players(), + ) + + self._series_length: int = classic.teams_series_length + self._ffa_series_length: int = classic.ffa_series_length + + self._tutorial_activity_instance = None + self._game_number = 0 + + # Our faux playlist + self._playlist = playlist + + self._current_game_spec: dict[str, Any] | None = None + self._next_game_spec: dict[str, Any] = self._playlist.pull_next() + self._next_game: type[GameActivity] = self._next_game_spec["resolved_type"] + + self._instantiate_next_game() + self.setactivity(newactivity(MultiTeamJoinActivity)) + + +# Classes for Teams autopilot and FFA autopilot +# I think they have to be separate in order to comply with `ba.GameActivity.supports_session_type()` +class RandFreeForAllSession(FreeForAllSession, RandomPlaySessionMixin): + def __init__(self): + playlist = RandomPlaylist(FreeForAllSession) + super(FreeForAllSession, self).__init__(playlist) + + +class RandDualTeamSession(DualTeamSession, RandomPlaySessionMixin): + def __init__(self): + playlist = RandomPlaylist(DualTeamSession) + super(DualTeamSession, self).__init__(playlist) + + +# The faux playlist that just picks games at random +class RandomPlaylist: + sessiontype: Session + all_games: list[GameActivity] + usable_games: list[GameActivity] + + last_game: str + + def __init__(self, sessiontype): + self.sessiontype = sessiontype + self.usable_games: list[GameActivity] = [ + gt + for gt in RandomPlaylist.all_games + if gt.supports_session_type(self.sessiontype) + ] + self.last_game = None + + def pull_next(self) -> dict[str, Any]: + """ + Generate a new game at random. + """ + + has_only_one_game = len(self.usable_games) == 1 + + while True: + game = choice(self.usable_games) + if game.name == self.last_game: + # Don't repeat the same game twice + if has_only_one_game: + # ...but don't freeze when there's only one game + break + else: + break + + self.last_game = game.name + game_map = choice(game.get_supported_maps(self.sessiontype)) + settings = { + s.name: s.default for s in game.get_available_settings(self.sessiontype) + } + settings["map"] = game_map + + if "Epic Mode" in settings: + # Throw in an Epic Mode once in a while + settings["Epic Mode"] = randint(0, 4) == 4 + + return {"resolved_type": game, "settings": settings} + + +# Adapted from plugin quick_custom_game. +# Hope you don't mind. +def patched__init__( + self, + sessiontype: type[Session], + transition: str | None = "in_right", + origin_widget: Widget | None = None, + playlist_select_context: PlaylistSelectContext | None = None, +): + width = 800 + height = 650 + + ui_scale = babase.app.ui_v1.uiscale + + y_offset = -95 if ui_scale is UIScale.SMALL else -35 if ui_scale is UIScale.MEDIUM else 115 + x_offset = 140 if ui_scale is UIScale.SMALL else 80 if ui_scale is UIScale.MEDIUM else 240 + + self.old__init__(sessiontype, transition, origin_widget) + # pylint: disable=protected-access + self._quick_game_button = buttonwidget( + parent=self._root_widget, + position=(width - 120 * 2 + x_offset, height - 132 + y_offset), + autoselect=True, + size=(80, 50), + scale=0.6 if ui_scale is UIScale.SMALL else 1.15, + text_scale=1.2, + label="Random", + on_activate_call=game_starter_factory(sessiontype), + color=(0.54, 0.52, 0.67), + textcolor=(0.7, 0.65, 0.7), + ) + +# Returns a function that starts the game + + +def game_starter_factory(sessiontype: type[Session]): + session: Union[RandFreeForAllSession, RandDualTeamSession] = None + + if issubclass(sessiontype, FreeForAllSession): + session = RandFreeForAllSession + elif issubclass(sessiontype, DualTeamSession): + session = RandDualTeamSession + else: + raise RuntimeError("Can't determine session type") + + def on_run(): + can_start = False + + def do_start(game_list): + nonlocal can_start + RandomPlaylist.all_games = game_list + if not can_start: # Don't start if the screen fade is still ongoing + can_start = True + else: + start() + + def has_faded(): + nonlocal can_start + if not can_start: # Don't start if it's still loading + can_start = True + else: + start() + + def start(): + babase.unlock_all_input() + new_host_session(session) + + babase.fade_screen(False, time=0.25, endcall=has_faded) + babase.lock_all_input() + babase.app.meta.load_exported_classes(GameActivity, do_start) + + return on_run + + +# ba_meta export babase.Plugin +class RandomPlayPlugin(babase.Plugin): + """ + A plugin that allows you to play randomly generated FFA or Teams matches by selecting a random minigame and map for each round. + This eliminates the need to set up long playlists to enjoy all your BombSquad content. + """ + + def __init__(self): + PlaylistBrowserWindow.old__init__ = PlaylistBrowserWindow.__init__ + PlaylistBrowserWindow.__init__ = patched__init__ diff --git a/plugins/utilities/rejoin.py b/plugins/utilities/rejoin.py new file mode 100644 index 000000000..d1fa9b641 --- /dev/null +++ b/plugins/utilities/rejoin.py @@ -0,0 +1,35 @@ +# ba_meta require api 9 +import bascenev1 as bs +from babase import Plugin as v +from bauiv1 import buttonwidget as z, gettexture as x +from bauiv1lib.ingamemenu import InGameMenuWindow as m + +m.i = m.p = 0 +k = bs.connect_to_party + + +def j(address, port=43210, print_progress=False): + try: + if bool(bs.get_connection_to_host_info_2()): + bs.disconnect_from_host() + except: + pass + m.i = address + m.p = port + k(m.i, m.p, print_progress) + + +def R(s): + def w(t, *f, **g): + z(parent=t._root_widget, size=(23, 26), icon=x('replayIcon'), + on_activate_call=bs.Call(j, m.i, m.p)) + return s(t, *f, **g) + return w + +# ba_meta export babase.Plugin + + +class byBordd(v): + def __init__(s): + m._refresh_in_game = R(m._refresh_in_game) + bs.connect_to_party = j diff --git a/plugins/utilities/replay.py b/plugins/utilities/replay.py new file mode 100644 index 000000000..106ec951a --- /dev/null +++ b/plugins/utilities/replay.py @@ -0,0 +1,1618 @@ +# Copyright 2025 - Solely by BrotherBoard +# Intended for personal use only +# Bug? Feedback? Telegram >> @BroBordd + +""" +Replay v3.0 - Simple replay player + +Experimental. Feedback is appreciated. +Adds a button to pause menu and watch menu. + +Features: +- Common features (pause/play/seek/speed/replay) +- Press on progress bar to seek anywhere +- Advanced free camera target control +- Ability to zoom in/out anywhere in the 3D world +- Instant replay start with progressive duration scanning +- Stream-like buffering (like YouTube) which plays while loading +- Good UI with detailed toast pop ups +- Ability to show/hide UI +- Uses threading for non-blocking duration calculation +- Dynamic progress bar that grows with estimated duration +- Visual scan progress indicator with alternating % and time display +""" + +from babase import Plugin +import bauiv1 as bui +from bauiv1 import CallPartial +import bascenev1 as bs +import _babase as _ba +import os +from time import strftime, gmtime, time +from random import uniform +from threading import Thread +from struct import unpack + + +class Replay: + VER = '3.0' + COL1 = (0.18, 0.18, 0.18) + COL2 = (1, 1, 1) + COL3 = (0, 1, 0) + COL4 = (0, 1, 1) + BUSY = False + + @classmethod + def is_busy(cls, value=None): + if value is None: + return cls.BUSY + cls.BUSY = value + + def __init__(self, source=None): + self.selected = self.replay_name = self.buffer = None + self.error = False + self.huffman = _Huffman() + self.parent = self.create_container( + src=source.get_screen_space_center(), + p=get_overlay_stack(), + size=(400, 500), + oac=lambda: ( + bui.containerwidget( + self.parent, transition='out_scale' if source and source.exists() else 'out_left'), + self.play_sound('laser'), + self.transition_sound.stop() + ) + ) + self.transition_sound = self.play_sound('powerup01') + self.create_text( + p=self.parent, + h_align='center', + text='Replay', + pos=(175, 460), + scale=2 + ) + scroll_y = 360 + scroll_parent = bui.scrollwidget( + parent=self.parent, + size=(scroll_y, scroll_y), + position=(25, 80) + ) + self.replays_dir = bui.get_replays_dir() + replays = [f for f in os.listdir(self.replays_dir) if f.endswith('.brp')] + scroll_height = 30 * len(replays) + scroll_content = bui.containerwidget( + parent=scroll_parent, + background=False, + size=(scroll_y, scroll_height) + ) + self.replay_widgets = [] + for idx, replay_file in enumerate(replays): + widget = self.create_text( + p=scroll_content, + click_activate=True, + selectable=True, + pos=(0, scroll_height - 30 * idx - 30), + text=replay_file, + maxwidth=scroll_y, + size=(scroll_y, 30), + color=self.COL2, + oac=CallPartial(self.highlight, idx, replay_file) + ) + self.replay_widgets.append(widget) + self.play_source = None + for i in range(3): + btn = self.create_button( + p=self.parent, + pos=(25 + 120 * i, 30), + size=(120, 40), + label=['Show', 'Copy', 'Run'][i], + oac=CallPartial(self.execute, [self.show, self.copy, self.play][i]), + icon=bui.gettexture(['folder', 'file', 'nextLevelIcon'][i]) + ) + if i == 2: + self.play_source = btn + + def play_sound(self, name): + sound = bui.getsound(name) + sound.play() + bui.apptimer(uniform(0.14, 0.17), sound.stop) + return sound + + def get_replay_path(self): + return os.path.join(self.replays_dir, self.replay_name) + + def copy(self): + self.play_sound('dingSmallHigh') + bui.clipboard_set_text(self.get_replay_path()) + bui.screenmessage('Copied replay path to clipboard!', color=self.COL3) + + def show(self): + bui.getsound('ding').play() + bui.screenmessage(self.get_replay_path(), color=self.COL3) + + def execute(self, func): + if self.selected is None: + show_warning('Select a replay!') + return + if bs.is_in_replay(): + show_warning('A replay is already running!') + return + return func() + + def highlight(self, idx, name): + if self.selected == idx: + self.play_source = self.replay_widgets[idx] + self.play() + return + self.selected = idx + self.replay_name = name + [bui.textwidget(w, color=self.COL2) for w in self.replay_widgets] + bui.textwidget(self.replay_widgets[idx], color=self.COL3) + + def play(self): + """Start replay immediately without waiting for duration calculation""" + if self.is_busy(): + return + self.is_busy(True) + bui.getsound('deek').play() + + # Start replay immediately + bs.set_replay_speed_exponent(0) + bui.fade_screen(1) + + # Create player with unknown duration + Player(path=self.get_replay_path(), duration=None) + self.is_busy(False) + + def load(self): + src = self.play_source.get_screen_space_center() + if self.play_source.get_widget_type() == 'text': + src = (src[0] - 170, src[1]) + self.parent_container = container = self.create_container( + src=src, + size=(300, 200), + p=get_overlay_stack() + ) + self.create_text( + p=container, + text='Player', + pos=(125, 150), + h_align='center', + scale=1.4 + ) + bui.spinnerwidget( + parent=container, + size=60, + position=(75, 100) + ) + self.status_text = self.create_text( + p=container, + text='Reading...', + pos=(115, 87) + ) + self.progress_text = self.create_text( + p=container, + pos=(125, 30), + maxwidth=240, + text=f'{self.replay_name} with total of {os.path.getsize(self.get_replay_path())} bytes\nstreaming bytes to pybrp_stream', + h_align='center' + ) + self.progress_text2 = self.create_text( + p=container, + maxwidth=240, + pos=(30, 20), + v_align='bottom' + ) + self.progress = [0, 1] + bui.apptimer(0.5, Thread(target=self.calculate).start) + bui.apptimer(0.5, self.update_progress) + self.wait_for_calculation(self.finish_calculation) + + def update_progress(self): + current, total = self.progress + bui.apptimer(0.1, self.update_progress) if (current != total) and (not self.error) else 0 + if not current: + return + percent = current / total * 100 + bar = '\u2588' * int(percent) + '\u2591' * int(100 - percent) + if not self.error: + try: + bui.textwidget(self.progress_text, text=bar) + bui.textwidget(self.progress_text2, text=f'{current} of {total} bytes read') + except: + return + + def calculate(self): + try: + self.buffer = get_replay_duration(self.huffman, self.get_replay_path(), self.progress) + except: + self.buffer = 0 + + def finish_calculation(self, duration): + bui.textwidget(self.status_text, text='Starting...' if duration else 'Wait what?') + bui.textwidget(self.progress_text2, + text=f'result was {duration} milleseconds') if duration else duration + if not duration: + self.error = True + bui.textwidget( + self.progress_text, text='pybrp returned zero duration, error?\nclosing this window in 5 seconds') + bui.textwidget(self.progress_text2, text='') + bui.apptimer(1 if duration else 5, CallPartial(self.start_player, duration)) + + def wait_for_calculation(self, callback, iterations=60): + if not iterations: + self.buffer = None + callback(None) + return + if self.buffer is not None: + result = self.buffer + self.buffer = None + callback(result) + return + bui.apptimer(0.5, CallPartial(self.wait_for_calculation, callback, iterations - 1)) + + def start_player(self, duration): + if duration == 0: + show_warning("Couldn't load replay!") + bui.containerwidget(self.parent_container, transition='out_scale') + self.is_busy(False) + return + bs.set_replay_speed_exponent(0) + bui.fade_screen(1) + Player(path=self.get_replay_path(), duration=duration) + self.is_busy(False) + + def create_button(self, p=None, oac=None, pos=None, **kwargs): + return bui.buttonwidget( + parent=p, + color=self.COL1, + textcolor=self.COL2, + on_activate_call=oac, + position=pos, + button_type='square', + enable_sound=False, + **kwargs + ) + + def create_container(self, p=None, pos=None, src=None, oac=None, **kwargs): + return bui.containerwidget( + color=self.COL1, + parent=p, + position=pos, + scale_origin_stack_offset=src, + transition='in_scale', + on_outside_click_call=oac, + **kwargs + ) + + def create_text(self, color=None, oac=None, p=None, pos=None, **kwargs): + return bui.textwidget( + parent=p, + position=pos, + color=color or self.COL2, + on_activate_call=oac, + **kwargs + ) + + +class Player: + TICK = 0.01 + COL0 = (0.5, 0, 0) + COL1 = (1, 0, 0) + COL2 = (0.5, 0.5, 0) + COL3 = (1, 1, 0) + COL4 = (0, 0.5, 0) + COL5 = (0, 1, 0) + COL6 = (0, 0.5, 0.5) + COL7 = (0, 1, 1) + COL8 = (0.6, 0.6, 0.6) + COL9 = (8, 0, 0) + COL10 = (0.5, 0.25, 0) + COL11 = (1, 0.5, 0) + COL12 = (0.5, 0.25, 0.5) + COL13 = (1, 0.5, 1) + COL14 = (0.5, 0.5, 0.5) + COL15 = (1, 1, 1) + COL16 = (0.1, 0.2, 0.4) + COL17 = (1, 1.7, 2) + COL18 = (0.45, 0.45, 0.45) + COL19 = (1, 0.8, 0) + + def __init__(self, path, duration): + self.path = path + self.duration_ms = duration # Will be None initially + self.duration_sec = None if duration is None else duration / 1000 + self.scanning = duration is None + self.scan_progress = [0, 1] + self.estimated_duration = 10.0 # Start with 10 second estimate + self.confirmed_duration_sec = 0 + self.paused = self.ui_hidden = self.camera_on = self.cinema_mode = self.manual_zoom = False + self.camera_look = None + self.replay_time = self.start_time = self.progress_val = 0 + self.camera_zoom = 1 + [setattr(self, attr, []) for attr in ['ui_widgets', 'camera_widgets', + 'hide_widgets', 'cinema_widgets', 'cinema_ui_widgets']] + + # Start replay immediately + bs.new_replay_session(path) + + width, height = bui.get_virtual_screen_size() + self.bar_height = 80 + self.parent = bui.containerwidget( + size=(width, self.bar_height), + stack_offset=(0, -height / 2 + self.bar_height / 2), + background=False + ) + self.background = bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('black'), + size=(width + 3, self.bar_height + 5), + position=(0, -2), + opacity=0.4 + ) + + self.create_ui() + self.create_hide_button() + self.speed = 1 + self.start_focus() + self.play() + + if self.scanning: + self.huffman = _Huffman() + self.scan_cycle = 0 # For alternating display + Thread(target=self.calculate_duration).start() + self.scan_timer = bui.AppTimer(0.1, self.update_scan_progress, repeat=True) + + def calculate_duration(self): + """Calculate duration in background thread with reduced load""" + import time as pytime + + try: + total_ms = 0 + last_yield = pytime.time() + + with open(self.path, 'rb') as f: + f.seek(0, 2) + file_size = f.tell() + self.scan_progress[1] = file_size + f.seek(6) + + while True: + current_pos = f.tell() + self.scan_progress[0] = current_pos + + # Yield control every 10ms to prevent freezing + current_time = pytime.time() + if current_time - last_yield > 0.01: + pytime.sleep(0.005) # Small sleep to reduce CPU load + last_yield = current_time + + b_data = f.read(1) + if not b_data: + break + + b1, comp_len = b_data[0], 0 + if b1 < 254: + comp_len = b1 + elif b1 == 254: + comp_len = int.from_bytes(f.read(2), 'little') + else: + comp_len = int.from_bytes(f.read(4), 'little') + + if comp_len == 0: + continue + + raw_msg = self.huffman.decompress(f.read(comp_len)) + + if not raw_msg or raw_msg[0] != 1: + continue + + sub_off = 1 + while sub_off + 2 <= len(raw_msg): + sub_size_bytes = raw_msg[sub_off:sub_off+2] + if len(sub_size_bytes) < 2: + break + sub_size = int.from_bytes(sub_size_bytes, 'little') + sub_off += 2 + if sub_off + sub_size > len(raw_msg): + break + sub_data = raw_msg[sub_off:sub_off+sub_size] + if len(sub_data) >= 2 and sub_data[0] == 0: + total_ms += sub_data[1] + sub_off += sub_size + + # Update confirmed duration (this is safe to seek to!) + self.confirmed_duration_sec = total_ms / 1000 + + # Update estimated duration based on progress + if current_pos > 1000: # After reading at least 1KB + progress_percent = current_pos / file_size + if progress_percent > 0: + estimated_total_ms = total_ms / progress_percent + self.estimated_duration = max(10.0, estimated_total_ms / 1000) + + self.scan_progress[0] = self.scan_progress[1] + self.duration_ms = total_ms + self.duration_sec = total_ms / 1000 + + except Exception as e: + self.duration_ms = 0 + self.duration_sec = 0 + finally: + self.scanning = False + + def update_scan_progress(self): + """Update duration text with scan progress - alternates between % and estimated time""" + if not self.scanning: + self.scan_timer = None + # Fade out loading bar + if hasattr(self, 'loading_bar') and self.loading_bar.exists(): + bui.imagewidget(self.loading_bar, opacity=0) + if self.main_bar.exists(): + bui.imagewidget(self.main_bar, opacity=0.6) + + # Update to show final duration with normal color + if hasattr(self, 'duration_time_text') and self.duration_time_text.exists(): + bui.textwidget( + self.duration_time_text, + text=format_time(self.duration_sec), + color=self.COL6 + ) + return + + # Show scanning progress + if hasattr(self, 'duration_time_text') and self.duration_time_text.exists(): + current, total = self.scan_progress + if total > 0: + percent = int((current / total) * 100) + + # Cycle between showing percentage and estimated time + self.scan_cycle += 1 + show_percent = (self.scan_cycle // 10) % 2 == 0 # Switch every second + + if show_percent: + # Show scan percentage + bui.textwidget( + self.duration_time_text, + text=f'Scan {percent}%', + color=self.COL6 + ) + else: + # Show estimated duration in orange + est_mins = int(self.estimated_duration // 60) + est_secs = int(self.estimated_duration % 60) + bui.textwidget( + self.duration_time_text, + text=format_time(self.estimated_duration), + color=self.COL19 # Orange color + ) + + # Update loading bar width based on scan progress + if hasattr(self, 'loading_bar') and self.loading_bar.exists(): + loading_width = (current / total) * self.progress_width + bui.imagewidget(self.loading_bar, size=(loading_width, 5)) + + def create_hide_button(self): + widgets = self.hide_widgets.append + self.hide_icons = ['\u25bc', '\u25b2'] + self.hide_button = self.create_button( + p=self.parent, + pos=(20, 15), + size=(50, 50), + oac=self.toggle_hide, + color=self.COL10 + ) + widgets(self.hide_button) + self.hide_text = bui.textwidget( + parent=self.parent, + text=self.hide_icons[self.ui_hidden], + position=(44, 30), + scale=2, + shadow=0.4, + color=self.COL11 + ) + widgets(self.hide_text) + widgets(bui.imagewidget( + parent=self.parent, + position=(18, 13), + size=(54, 54), + color=self.COL10, + texture=bui.gettexture('white'), + opacity=0.4 + )) + + def destroy_hide_button(self): + [w.delete() for w in self.hide_widgets] + self.hide_widgets.clear() + + def create_ui(self): + """Modified to handle unknown duration initially""" + self.ui_visible = True + widgets = self.ui_widgets.append + width, height = bui.get_virtual_screen_size() + bar_height = self.bar_height + parent = self.parent + + # Exit button + widgets(self.create_button( + p=parent, + pos=(width - 65, 15), + size=(50, 50), + color=self.COL0, + oac=self.exit + )) + color = self.COL1 + widgets(bui.imagewidget( + parent=parent, + texture=bui.gettexture('crossOut'), + color=(color[0] * 10, color[1] * 10, color[2] * 10), + position=(width - 60, 20), + size=(40, 40) + )) + + # Speed buttons + for i in range(2): + arrow = ['FAST_FORWARD_BUTTON', 'REWIND_BUTTON'][i] + pos = (width - 130 - 260 * i, 15) + widgets(self.create_button( + p=parent, + pos=pos, + size=(50, 50), + color=self.COL2, + oac=CallPartial(self.change_speed, [1, -1][i]), + repeat=True + )) + widgets(bui.textwidget( + parent=parent, + text=bui.charstr(getattr(bui.SpecialChar, arrow)), + color=self.COL3, + position=(pos[0] - 2, pos[1] + 13), + h_align='center', + v_align='center', + scale=1.8, + shadow=0.3 + )) + + # Seek buttons + for i in range(2): + arrow = ['RIGHT_ARROW', 'LEFT_ARROW'][i] + pos = (width - 195 - 130 * i, 15) + widgets(self.create_button( + p=parent, + pos=pos, + size=(50, 50), + color=self.COL4, + oac=CallPartial(self.seek, [1, -1][i]), + repeat=True + )) + widgets(bui.textwidget( + parent=parent, + text=bui.charstr(getattr(bui.SpecialChar, arrow)), + color=self.COL5, + position=(pos[0] - 1, pos[1] + 12), + h_align='center', + v_align='center', + scale=1.7, + shadow=0.2 + )) + + # Pause button + pos = (width - 260, 15) + widgets(self.create_button( + p=parent, + pos=pos, + size=(50, 50), + color=self.COL6, + oac=self.toggle_pause + )) + self.pause_text = bui.textwidget( + parent=parent, + color=self.COL7, + position=(pos[0] + 12, pos[1] + 11), + scale=1.5, + shadow=0.3 + ) + widgets(self.pause_text) + self.toggle_pause(dry=True) + + # Restart button + pos = (width - 455, 15) + widgets(self.create_button( + p=parent, + pos=pos, + size=(50, 50), + color=self.COL12, + oac=self.restart + )) + color = self.COL13 + scale = 1.5 + widgets(bui.imagewidget( + parent=parent, + texture=bui.gettexture('replayIcon'), + color=(color[0] * scale, color[1] * scale, color[2] * scale), + position=(pos[0] + 2, pos[1] + 1), + size=(47, 47), + )) + + # Progress bar + pos = (285, bar_height / 2 - 2) + self.progress_width = width - 790 + self.main_bar = bui.imagewidget( + parent=parent, + texture=bui.gettexture('white'), + size=(self.progress_width, 5), + position=pos, + opacity=0.2 if self.scanning else 0.6, + color=self.COL8 + ) + widgets(self.main_bar) + + # Secondary progress bar + self.loading_bar = bui.imagewidget( + parent=parent, + texture=bui.gettexture('white'), + size=(0, 5), + position=pos, + opacity=0.5, + color=self.COL18 + ) + widgets(self.loading_bar) + + # Nub + self.nub_pos = (pos[0] - 24, pos[1] - 22) + self.nub = bui.imagewidget( + parent=parent, + texture=bui.gettexture('nub'), + size=(50, 50), + position=self.nub_pos, + opacity=0.4, + color=self.COL9 + ) + widgets(self.nub) + + # Time displays + self.current_time_text = bui.textwidget( + parent=parent, + position=(155, 40), + color=self.COL7, + text=format_time(self.replay_time - self.start_time), + maxwidth=100 + ) + widgets(self.current_time_text) + + # Duration display - alternates between scan% and estimated time while scanning + self.duration_time_text = bui.textwidget( + parent=parent, + position=(155, 11), + text='Scan 0%' if self.scanning else format_time(self.duration_sec), + color=self.COL6, + maxwidth=100 + ) + widgets(self.duration_time_text) + + # Seekbar sensors + sensor_x, sensor_y = (285, 15) + sensor_count = 100 + tile_width = self.progress_width / sensor_count + for i in range(sensor_count): + widgets(bui.buttonwidget( + label='', + parent=parent, + position=(sensor_x + tile_width * i, sensor_y), + size=(tile_width, 50), + texture=bui.gettexture('empty'), + enable_sound=False, + on_activate_call=CallPartial(self.jump, i / sensor_count), + selectable=False + )) + + # Camera button + widgets(self.create_button( + p=self.parent, + pos=(85, 15), + size=(50, 50), + color=self.COL14, + oac=self.toggle_camera + )) + color = self.COL15 + scale = 1.5 + widgets(bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('achievementOutline'), + position=(88, 18), + color=(color[0] * scale, color[1] * scale, color[2] * scale), + size=(45, 45) + )) + + # Info panel + info_w, info_h = (443, 98) + self.info_bg = bui.imagewidget( + texture=bui.gettexture('white'), + position=(width - 456, 100), + parent=parent, + size=(info_w, info_h), + opacity=0 + ) + widgets(self.info_bg) + self.info_title = bui.textwidget( + position=(width - info_w + 182.5, info_h + 64), + h_align='center', + scale=1.2, + parent=parent, + maxwidth=info_w - 20 + ) + widgets(self.info_title) + self.info_text = bui.textwidget( + position=(width - info_w + 182.5, info_h + 10), + h_align='center', + parent=parent, + maxwidth=info_w - 20 + ) + widgets(self.info_text) + + def create_camera_ui(self): + widgets = self.camera_widgets.append + cam_x, cam_y = (19, 100) + self.camera_bg = bui.imagewidget( + parent=self.parent, + size=(235, 260), + position=(cam_x, cam_y), + texture=bui.gettexture('white'), + color=self.COL14, + opacity=0.05 + ) + widgets(self.camera_bg) + + self.camera_bg2 = bui.imagewidget( + parent=self.parent, + size=(235, 100), + opacity=0.05, + position=(cam_x, cam_y + 267), + color=self.COL14, + texture=bui.gettexture('white') + ) + widgets(self.camera_bg2) + self.fade_camera() + widgets(bui.textwidget( + parent=self.parent, + h_align='center', + v_align='center', + text='To maintain animated\nsmooth camera, keep\nzoom at auto.', + maxwidth=205, + max_height=190, + position=(cam_x + 92, cam_y + 300), + color=self.COL15 + )) + + widgets(self.create_button( + p=self.parent, + label='Reset', + color=self.COL14, + textcolor=self.COL15, + size=(205, 30), + pos=(cam_x + 15, cam_y + 7), + oac=self.reset_camera + )) + + widgets(bui.imagewidget( + parent=self.parent, + size=(219, 2), + position=(cam_x + 8, cam_y + 44), + texture=bui.gettexture('white'), + color=self.COL15, + opacity=0.6 + )) + + widgets(self.create_button( + p=self.parent, + label='Look', + pos=(cam_x + 15, cam_y + 53), + color=self.COL14, + textcolor=self.COL15, + size=(205, 30), + oac=self.look + )) + self.look_text = bui.textwidget( + parent=self.parent, + text=str(round_tuple(self.camera_look) if self.camera_look else 'players'), + v_align='center', + h_align='center', + position=(cam_x + 92, cam_y + 90), + color=self.COL14, + maxwidth=205, + max_height=40 + ) + widgets(self.look_text) + widgets(bui.textwidget( + parent=self.parent, + text='Currently looking at:', + v_align='center', + h_align='center', + position=(cam_x + 92, cam_y + 120), + color=self.COL15, + maxwidth=205, + max_height=40 + )) + + widgets(bui.imagewidget( + parent=self.parent, + size=(219, 2), + position=(cam_x + 8, cam_y + 154), + texture=bui.gettexture('white'), + color=self.COL15, + opacity=0.6 + )) + + [widgets(self.create_button( + p=self.parent, + label=['-', '+'][i], + pos=(cam_x + 13 + 113 * i, cam_y + 163), + color=self.COL14, + textcolor=self.COL15, + size=(98, 30), + repeat=True, + oac=CallPartial(self.zoom, [1, -1][i]) + )) for i in [0, 1]] + self.zoom_text = bui.textwidget( + parent=self.parent, + text=f'x{round(0.5 ** (self.camera_zoom - 1), 2)}' if self.camera_zoom != 1 else 'x1.0' if self.manual_zoom else 'auto', + v_align='center', + h_align='center', + position=(cam_x + 92, cam_y + 200), + color=self.COL14, + maxwidth=205, + max_height=40 + ) + widgets(self.zoom_text) + widgets(bui.textwidget( + parent=self.parent, + text='Current zoom:', + v_align='center', + h_align='center', + position=(cam_x + 92, cam_y + 227), + color=self.COL15, + maxwidth=205, + max_height=40 + )) + + def zoom(self, direction): + new_zoom = round(self.camera_zoom + direction * 0.05, 2) + if self.camera_zoom == 1 and not self.manual_zoom: + _ba.set_camera_manual(True) + self.camera_pos = _ba.get_camera_position() + self.camera_look = _ba.get_camera_target() + bui.textwidget(self.look_text, text=str(round_tuple(self.camera_look))) + if new_zoom == 1 and not self.manual_zoom: + _ba.set_camera_manual(False) + self.camera_look = None + bui.textwidget(self.look_text, text='players') + self.camera_zoom = new_zoom + bui.textwidget( + self.zoom_text, text=f'x{round(0.5 ** (new_zoom - 1), 2)}' if new_zoom != 1 else 'x1.0' if self.manual_zoom else 'auto') + self.apply_zoom() + + def look(self): + self.destroy_ui() + self.destroy_hide_button() + self.fade_hide_button(0.4, -0.1) + self.look_backup = self.camera_look + self.pos_backup = _ba.get_camera_position() + self.manual_backup = self.manual_zoom + self.create_cinema_sensors() + self.create_cinema_ui() + self.create_cinema_button() + self.create_cinema_indicator() + + def update_look(self, dx, dy): + origin = self.camera_look or _ba.get_camera_target() + scale = 0.7 * self.camera_zoom + self.camera_look = new_look = (origin[0] + dx * scale, origin[1] + dy * scale, origin[2]) + 0 if self.cinema_mode else bui.textwidget(self.cinema_text, text=str(round_tuple(new_look))) + self.camera_pos = _ba.get_camera_position() + if self.camera_zoom != 1: + self.manual_zoom = True + self.camera_zoom = 1 + + def start_focus(self): + self.focus_timer = bui.AppTimer(0.01, self.focus, repeat=True) + + def focus(self): + _ba.set_camera_target(*self.camera_look) if self.camera_look else 0 + + def apply_zoom(self): + if self.camera_zoom == 1 and not self.manual_zoom: + return + zoom = self.camera_zoom + target_x, target_y, target_z = _ba.get_camera_target() + pos_x, pos_y, pos_z = self.camera_pos + new_pos_x = target_x + (pos_x - target_x) * zoom + new_pos_y = target_y + (pos_y - target_y) * zoom + new_pos_z = target_z + (pos_z - target_z) * zoom + _ba.set_camera_position(new_pos_x, new_pos_y, new_pos_z) + + def stop_focus(self): + self.focus_timer = None + + def save_cinema(self): + self.exit_cinema() + + def exit_cinema(self): + self.destroy_cinema() + self.create_ui() + self.create_hide_button() + self.fade_hide_button(0, 0.1) + self.update_progress_ui() + + def create_cinema_sensors(self): + width, height = bui.get_virtual_screen_size() + tile = 50 + cols = int(width / tile) + rows = int(height / tile) + half_cols = cols / 2 + half_rows = rows / 2 + [self.cinema_widgets.append(bui.buttonwidget( + parent=self.parent, + size=(tile, tile), + position=(i * tile, j * tile), + texture=bui.gettexture('empty'), + enable_sound=False, + on_activate_call=CallPartial(self.update_look, i - half_cols, j - half_rows), + label='', + repeat=True + )) + for i in range(cols) + for j in range(rows)] + + def create_cinema_ui(self): + widgets = self.cinema_ui_widgets.append + widgets(bui.imagewidget( + parent=self.parent, + position=(0, 3), + color=self.COL14, + opacity=0.4, + texture=bui.gettexture('white'), + size=(232, 190) + )) + + widgets(self.create_button( + p=self.parent, + pos=(14, 50), + size=(204, 30), + label='Target Players', + color=self.COL14, + textcolor=self.COL15, + oac=self.target_players + )) + widgets(self.create_button( + p=self.parent, + pos=(10, 90), + color=self.COL14, + label='Cancel', + size=(99, 30), + textcolor=self.COL15, + oac=self.cancel_cinema + )) + widgets(self.create_button( + p=self.parent, + pos=(123, 90), + color=self.COL14, + label='Save', + size=(99, 30), + textcolor=self.COL15, + oac=self.save_cinema + )) + + widgets(bui.textwidget( + parent=self.parent, + position=(90, 160), + color=self.COL15, + text='Currently looking at:', + h_align='center', + maxwidth=220 + )) + self.cinema_text = bui.textwidget( + parent=self.parent, + position=(90, 130), + color=self.COL14, + h_align='center', + text=str(round_tuple(self.camera_look) if self.camera_look else 'players') + ) + widgets(self.cinema_text) + + widgets(bui.imagewidget( + parent=self.parent, + position=(0, 200), + color=self.COL14, + opacity=0.4, + texture=bui.gettexture('white'), + size=(232, 110) + )) + widgets(bui.textwidget( + parent=self.parent, + position=(90, 240), + text='Longpress anywhere\nto look around. Tap on \nsomething to look at it.\nPause for calmer control!', + h_align='center', + v_align='center', + maxwidth=225, + max_height=105 + )) + + width, height = bui.get_virtual_screen_size() + crosshair = 20 + [widgets(bui.imagewidget( + parent=self.parent, + position=(width / 2, height / 2 - crosshair / 2 + crosshair * + 0.1) if i else (width / 2 - crosshair / 2, height / 2 + crosshair * 0.1), + size=(3, crosshair * 1.15) if i else (crosshair * 1.15, 3), + color=self.COL1, + texture=bui.gettexture('white') + )) for i in [0, 1]] + + offset = 60 + for j in range(2): + widgets(bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('white'), + color=self.COL1, + position=(width / 2 + [-offset, offset - crosshair][j], height / 2 + offset), + size=(crosshair * 1.1, 3) + )) + + for j in range(2): + widgets(bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('white'), + color=self.COL1, + position=(width / 2 + offset, height / 2 + + [offset - crosshair, -offset + crosshair * 0.3][j]), + size=(3, crosshair * +1.1) + )) + + for j in range(2): + widgets(bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('white'), + color=self.COL1, + position=(width / 2 + [-offset, offset - crosshair][j], + height / 2 - crosshair / 2 - offset + crosshair * 0.8), + size=(crosshair * 1.1, 3) + )) + + for j in range(2): + widgets(bui.imagewidget( + parent=self.parent, + texture=bui.gettexture('white'), + color=self.COL1, + position=(width / 2 - offset, height / 2 + + [offset - crosshair, -offset + crosshair * 0.3][j]), + size=(3, crosshair * 1.1) + )) + + def destroy_cinema_ui(self): + [w.delete() for w in self.cinema_ui_widgets] + + def toggle_cinema_hide(self): + if getattr(self, 'cinema_busy', 0): + return + self.cinema_mode = not self.cinema_mode + self.cinema_busy = True + if self.cinema_mode: + self.animate_cinema(204, 14, -1) + bui.buttonwidget(self.cinema_button, label=bui.charstr(bui.SpecialChar.UP_ARROW)) + self.destroy_cinema_ui() + else: + bui.buttonwidget(self.cinema_button, texture=bui.gettexture('white')) + self.animate_cinema(36, 7, 1) + bui.imagewidget(self.cinema_indicator, opacity=0) + + def create_cinema_indicator(self): + self.cinema_indicator = bui.imagewidget( + parent=self.parent, + position=(7, 8), + color=self.COL14, + opacity=0, + size=(36, 33), + texture=bui.gettexture('white') + ) + self.cinema_widgets.append(self.cinema_indicator) + + def create_cinema_button(self): + self.cinema_button = self.create_button( + p=self.parent, + pos=(14, 10), + color=self.COL14, + label='Cinema Mode', + size=(204, 30), + textcolor=self.COL15, + oac=self.toggle_cinema_hide + ) + self.cinema_widgets.append(self.cinema_button) + + def animate_cinema(self, width_val, x_val, direction): + width_val += (163 / 35) * direction + x_val += 0.2 * direction + bui.buttonwidget(self.cinema_button, size=(width_val, 30), position=(x_val, 10)) + if not (14 >= x_val >= 7): + self.cinema_busy = False + if self.cinema_mode: + bui.buttonwidget(self.cinema_button, texture=bui.gettexture('empty')) + bui.imagewidget(self.cinema_indicator, opacity=0.4) + else: + self.create_cinema_ui() + self.cinema_button.delete() + self.create_cinema_button() + bui.buttonwidget(self.cinema_button, label='Cinema Mode') + return + bui.apptimer(0.004, CallPartial(self.animate_cinema, width_val, x_val, direction)) + + def destroy_cinema(self): + self.destroy_cinema_ui() + [w.delete() for w in self.cinema_widgets] + + def get_all_widgets(self): + return self.get_deletable_widgets() + self.hide_widgets + self.cinema_widgets + + def target_players(self): + self.camera_look = None + bui.textwidget(self.cinema_text, text='players') + if self.camera_zoom != 1 or self.manual_zoom: + self.camera_zoom = 1 + self.manual_zoom = False + _ba.set_camera_manual(False) + + def cancel_cinema(self): + self.camera_look = self.look_backup + self.camera_pos = self.pos_backup + self.manual_zoom = self.manual_backup + if self.camera_zoom != 1 or self.manual_zoom: + _ba.set_camera_manual(True) + _ba.set_camera_position(*self.camera_pos) + self.exit_cinema() + + def toggle_camera(self): + if self.camera_on: + self.camera_on = False + [w.delete() for w in self.camera_widgets] + self.camera_widgets.clear() + self.fade_camera(0.4, -0.1) + else: + self.camera_on = True + self.create_camera_ui() + + def reset_camera(self): + _ba.set_camera_manual(False) + self.camera_look = None + self.manual_zoom = False + self.camera_zoom = 1 + bui.textwidget(self.look_text, text='players') + bui.textwidget(self.zoom_text, text='auto') + + def fade_camera(self, opacity=0, delta=0.1): + if opacity > 0.4 or opacity < 0: + if delta < 0: + self.camera_bg.delete() + return + if not self.camera_bg.exists(): + return + bui.imagewidget(self.camera_bg, opacity=opacity) + bui.imagewidget(self.camera_bg2, opacity=opacity) + bui.apptimer(0.02, CallPartial(self.fade_camera, opacity + delta, delta)) + + def restart(self): + self.loop() + self.fix_pause() + self.show_message('Replay', f'Version {Replay.VER} BETA', self.COL12, self.COL13) + + def destroy_ui(self): + self.ui_visible = self.camera_on = False + [w.delete() for w in self.get_deletable_widgets()] + self.ui_widgets.clear() + self.camera_widgets.clear() + + def get_deletable_widgets(self): + return self.ui_widgets + self.camera_widgets + + def toggle_hide(self): + if getattr(self, 'hide_busy', 0): + return + self.hide_busy = True + if getattr(self, 'exit_timer', 0) and getattr(self, 'exit_ready', 0): + self.exit_ready = self.exit_timer = False + self.info_timer = None + self.ui_hidden = hidden = not self.ui_hidden + bui.textwidget(self.hide_text, text=self.hide_icons[hidden]) + if hidden: + bui.apptimer(0.2, lambda: bui.buttonwidget( + self.hide_button, texture=bui.gettexture('empty'))) + self.fade_hide_button(0.4, -0.05) + self.destroy_ui() + else: + bui.buttonwidget(self.hide_button, texture=bui.gettexture('white')) + self.fade_hide_button(0, 0.05) + self.create_ui() + self.update_progress_ui() + bui.apptimer(0.21, CallPartial(setattr, self, 'hide_busy', 0)) + + def fade_hide_button(self, opacity=0, delta=0.1): + if opacity > 0.4 or opacity < 0: + return + if not self.background.exists(): + return + bui.imagewidget(self.background, opacity=opacity) + bui.apptimer(0.02, CallPartial(self.fade_hide_button, opacity + delta, delta)) + + def show_message(self, title, text, color1, color2): + if getattr(self, 'exit_timer', 0) and getattr(self, 'exit_ready', 0): + self.exit_ready = self.exit_timer = False + self.info_timer = None + bui.imagewidget(self.info_bg, color=color1) + bui.textwidget(self.info_title, text=title, color=color2) + bui.textwidget(self.info_text, text=text, color=color2) + self.fade_info() + self.info_timer = bui.AppTimer(1.5, self.hide_message) + + def hide_message(self): + self.fade_info(0.7, -0.1) + [bui.textwidget(w, text='') for w in [self.info_title, self.info_text] if w.exists()] + + def fade_info(self, opacity=0, delta=0.1): + if opacity > 0.7 or opacity < 0: + return + if not self.info_bg.exists(): + return + bui.imagewidget(self.info_bg, opacity=opacity) + bui.apptimer(0.02, CallPartial(self.fade_info, opacity + delta, delta)) + + def toggle_pause(self, dry=False, silent=False): + if not dry: + self.paused = not self.paused + icon = bui.charstr(getattr(bui.SpecialChar, ['PAUSE', 'PLAY'][self.paused] + '_BUTTON')) + bui.textwidget(self.pause_text, text=icon) + if not dry: + if not silent: + self.show_message(['Resume', 'Pause'][self.paused], os.path.basename( + self.path) + f' of {os.path.getsize(self.path)} bytes', self.COL6, self.COL7) + if self.paused: + self.stop() + bs.pause_replay() + else: + self.play() + bs.resume_replay() + + def fix_pause(self): + if not self.paused: + return + self.toggle_pause(silent=True) + bui.apptimer(0.02, CallPartial(self.toggle_pause, silent=True)) + + def update_clock(self): + current = time() + elapsed = current - self.real_time + self.real_time = current + self.replay_time += elapsed * self.speed + + def change_speed(self, direction): + new_exp = bs.get_replay_speed_exponent() + direction + bs.set_replay_speed_exponent(new_exp) + self.speed = 2 ** new_exp + label = 'Snail Mode' if self.speed == 0.0625 else 'Slow Motion' if self.speed < 1 else 'Quake Pro' if self.speed == 16 else 'Fast Motion' if self.speed > 1 else 'Normal Speed' + self.show_message(label, f'Current exponent: x{self.speed}', self.COL2, self.COL3) + + def play(self): + self.real_time = time() + self.update_clock() + self.start_progress_timer() + self.clock_timer = bui.AppTimer(self.TICK, self.update_clock, repeat=True) + + def stop(self): + self.clock_timer = None + self.stop_progress_timer() + + def stop_progress_timer(self): + self.progress_timer = None + + def start_progress_timer(self): + self.progress_timer = bui.AppTimer(self.TICK, self.update_progress_ui, repeat=True) + + def seek(self, direction): + """Seek with confirmed duration checking""" + label = ['Forward by', 'Rewind by'][direction == -1] + amount = direction * self.speed + + # Use estimated duration for calculating seek amount + if self.scanning: + seek_base = self.estimated_duration + else: + seek_base = self.duration_sec + + amount = (seek_base / 20) * amount + new_time = (self.replay_time - self.start_time) + amount + + # Check if trying to seek beyond confirmed duration while scanning + if self.scanning and new_time > self.confirmed_duration_sec: + current, total = self.scan_progress + percent = int((current / total) * 100) if total > 0 else 0 + self.show_message('Buffering...', f'{percent}% loaded', self.COL2, self.COL3) + return + + # Use appropriate duration for loop check + max_duration = self.confirmed_duration_sec if self.scanning else self.duration_sec + + if (new_time >= max_duration) or (new_time <= 0): + self.loop() + else: + self.start_time = self.replay_time - new_time + self.reset_replay() + bs.seek_replay(new_time) + + self.real_time = time() + self.fix_pause() + amount = abs(round(amount, 2)) + self.show_message( + 'Seek', label + f" {amount} second{['s', ''][amount == 1]}", self.COL4, self.COL5) + + def jump(self, percent): + """Jump with WYSIWYG behavior - percent is based on ESTIMATED duration, checked against CONFIRMED""" + # Calculate target time based on ESTIMATED duration (what user sees on bar) + if self.scanning: + target_time = self.estimated_duration * percent + else: + target_time = self.duration_sec * percent + + # Check if trying to jump beyond CONFIRMED duration while scanning + if self.scanning and target_time > self.confirmed_duration_sec: + current, total = self.scan_progress + scan_percent = int((current / total) * 100) if total > 0 else 0 + self.show_message('Buffering...', f'{scan_percent}% loaded', self.COL2, self.COL3) + return + + self.start_time = self.replay_time - target_time + self.reset_replay() + bs.seek_replay(target_time) + self.real_time = time() + self.fix_pause() + + def exit(self): + if getattr(self, 'exit_ready', 0): + self.confirm_exit() + return + self.show_message('Exit', 'Press again to confirm', self.COL0, self.COL1) + self.exit_ready = True + self.exit_timer = bui.AppTimer(1.5, CallPartial(setattr, self, 'exit_ready', False)) + + def confirm_exit(self): + bui.fade_screen(0, time=0.75, endcall=CallPartial(bui.fade_screen, 1, time=0.75)) + bui.getsound('deek').play() + return_to_menu() + self.stop() + self.stop_focus() + self.exit_timer = None + _ba.set_camera_manual(False) + + def update_progress_ui(self): + """Modified to handle scanning state with growing progress bar""" + elapsed = self.replay_time - self.start_time + + # Determine the current max duration to check against + if self.scanning: + max_duration = self.estimated_duration + elif self.duration_sec: + max_duration = self.duration_sec + else: + max_duration = None + + # Check if elapsed exceeds duration and loop if needed + if max_duration and elapsed >= max_duration: + self.loop() + elapsed = 0 # Reset elapsed after loop + + nub_x, nub_y = self.nub_pos + + # Calculate progress + if self.scanning: + # Use estimated duration for progress bar + if elapsed < self.estimated_duration: + progress = (elapsed / self.estimated_duration) * self.progress_width + else: + # Cap at 100% if somehow elapsed exceeds estimate + progress = self.progress_width + elif not self.duration_sec: + # Fallback if scan failed + progress = min(elapsed / max(elapsed, 1) * self.progress_width * + 0.1, self.progress_width * 0.1) + else: + # Normal progress calculation, cap at 100% + progress = min((elapsed / self.duration_sec) * self.progress_width, self.progress_width) + + try: + bui.imagewidget(self.nub, position=(nub_x + progress, nub_y)) + bui.textwidget(self.current_time_text, text=format_time(elapsed)) + except ReferenceError: + pass + + def reset_replay(self): + bs.seek_replay(-10 ** 10) + + def loop(self): + self.start_time = self.replay_time = 0 + self.reset_replay() + + def create_button(self, label='', p=None, oac=None, pos=None, texture='white', **kwargs): + return bui.buttonwidget( + parent=p, + on_activate_call=oac, + position=pos, + label=label, + texture=bui.gettexture(texture), + enable_sound=False, + **kwargs + ) + +# Tools + + +def get_ui_scale(small, medium, large=None): + scale = bui.app.ui_v1.uiscale + return small if scale is bui.UIScale.SMALL else medium if scale is bui.UIScale.MEDIUM else (large or medium) + + +def return_to_menu(): return bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) +def show_warning(text): return (bui.getsound('block').play() + or 1) and bui.screenmessage(text, color=(1, 1, 0)) + + +def format_time(seconds): return strftime('%H:%M:%S', gmtime(seconds)) +def get_overlay_stack(): return bui.get_special_widget('overlay_stack') +def round_tuple(tup): return type(tup)([round(val, 1) for val in tup]) + + +# pybrp +def Z(_): return [0]*_ + + +def G_FREQS(): return [ + 101342, 9667, 3497, 1072, 0, 3793, *Z(2), 2815, 5235, *Z(3), 3570, *Z(3), + 1383, *Z(3), 2970, *Z(2), 2857, *Z(8), 1199, *Z(30), + 1494, 1974, *Z(12), 1351, *Z(122), 1475, *Z(65) +] + + +class _Huffman: + class _N: + def __init__(self): + self.l, self.r, self.p, self.f = -1, -1, 0, 0 + + def __init__(self): + self.nodes = [self._N()for _ in range(511)] + gf = G_FREQS() + for i in range(256): + self.nodes[i].f = gf[i] + nc = 256 + while nc < 511: + s1, s2 = -1, -1 + i = 0 + while self.nodes[i].p != 0: + i += 1 + s1 = i + i += 1 + while self.nodes[i].p != 0: + i += 1 + s2 = i + i += 1 + while i < nc: + if self.nodes[i].p == 0: + if self.nodes[s1].f > self.nodes[s2].f: + if self.nodes[i].f < self.nodes[s1].f: + s1 = i + elif self.nodes[i].f < self.nodes[s2].f: + s2 = i + i += 1 + self.nodes[nc].f = self.nodes[s1].f+self.nodes[s2].f + self.nodes[s1].p = self.nodes[s2].p = nc-255 + self.nodes[nc].r, self.nodes[nc].l = s1, s2 + nc += 1 + + def decompress(self, src): + if not src: + return b'' + rem, comp = src[0] & 15, src[0] >> 7 + if not comp: + return src + out, ptr, l = bytearray(), src[1:], len(src) + bl = ((l-1)*8)-rem + bit = 0 + while bit < bl: + m_bit = (ptr[bit >> 3] >> (bit & 7)) & 1 + bit += 1 + if m_bit: + n = 510 + while n >= 256: + if bit >= bl: + raise ValueError("Incomplete Huffman code") + p_bit = (ptr[bit >> 3] >> (bit & 7)) & 1 + bit += 1 + n = self.nodes[n].l if p_bit == 0 else self.nodes[n].r + out.append(n) + else: + if bit+8 > bl: + break + bi, b_in_b = bit >> 3, bit & 7 + val = ptr[bi]if b_in_b == 0 else (ptr[bi] >> b_in_b) | (ptr[bi+1] << (8-b_in_b)) + out.append(val & 255) + bit += 8 + return bytes(out) + + +def get_replay_duration(_h, brp_path, progress): + total_ms = 0 + with open(brp_path, 'rb') as f: + f.seek(0, 2) + progress[1] = f.tell() + f.seek(6) + while True: + progress[0] = f.tell() + b_data = f.read(1) + if not b_data: + break + b1, comp_len = b_data[0], 0 + if b1 < 254: + comp_len = b1 + elif b1 == 254: + comp_len = int.from_bytes(f.read(2), 'little') + else: + comp_len = int.from_bytes(f.read(4), 'little') + if comp_len == 0: + continue + raw_msg = _h.decompress(f.read(comp_len)) + if not raw_msg or raw_msg[0] != 1: + continue + sub_off = 1 + while sub_off + 2 <= len(raw_msg): + sub_size_bytes = raw_msg[sub_off:sub_off+2] + if len(sub_size_bytes) < 2: + break + sub_size = int.from_bytes(sub_size_bytes, 'little') + sub_off += 2 + if sub_off + sub_size > len(raw_msg): + break + sub_data = raw_msg[sub_off:sub_off+sub_size] + if len(sub_data) >= 2 and sub_data[0] == 0: + total_ms += sub_data[1] + sub_off += sub_size + progress[0] = progress[1] + return total_ms + +# brobord collide grass +# ba_meta require api 9 +# ba_meta export babase.Plugin + + +class byBordd(Plugin): + def has_settings_ui(c=0): return True + def show_settings_ui(c=0, w=None): return Replay(source=w) + + def __init__(self): + from bauiv1lib.ingamemenu import InGameMenuWindow as ingame + orig_refresh = getattr(ingame, '_refresh_in_game') + setattr(ingame, '_refresh_in_game', lambda window, *args, ** + kwargs: (self.add_button(window), orig_refresh(window, *args, **kwargs))[1]) + from bauiv1lib.watch import WatchWindow as watch + orig_init = getattr(watch, '__init__') + setattr(watch, '__init__', lambda window, *args, ** + kwargs: (orig_init(window, *args, **kwargs), self.add_button(window, 1))[0]) + + def add_button(self, window, watch_mode=0): + if watch_mode: + btn_x = window._width / 2 + get_ui_scale(window._scroll_width * -0.5 + 93, 0) + 100 + btn_y = window.yoffs - get_ui_scale(63, 10) - 25 + self.button = Replay.create_button( + Replay, + p=window._root_widget, + label='Replay', + pos=(btn_x, btn_y) if watch_mode else (-70, 0), + icon=bui.gettexture('replayIcon'), + iconscale=1.6 if watch_mode else 0.8, + size=(140, 50) if watch_mode else (90, 35), + oac=lambda: Replay(source=self.button), + id='Replay' + ) diff --git a/plugins/utilities/sandbox.py b/plugins/utilities/sandbox.py new file mode 100644 index 000000000..00770d24d --- /dev/null +++ b/plugins/utilities/sandbox.py @@ -0,0 +1,5941 @@ +import babase as ba +import _babase as _ba # music control +from bauiv1lib.ingamemenu import InGameMenuWindow as igm +import bauiv1 as bui +import bascenev1 as bs +from bascenev1 import broadcastmessage as push, get_foreground_host_activity as ga +from bauiv1lib import popup # Pickers +from typing import Any, cast, Sequence, Optional, Callable # UI control +import math # floating name +from bauiv1lib.colorpicker import ColorPicker +import random # random attrs +from bascenev1lib.actor.spazbot import SpazBot, SpazBotSet +from bascenev1lib.actor.spaz import Spaz +from bascenev1lib.actor.bomb import Bomb, BombFactory +import weakref # get map (bot) +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.popuptext import PopupText # my unfunny popups +from bauiv1lib.tabs import TabRow # BOTS and USERS +from bascenev1lib.actor.powerupbox import PowerupBox +from bascenev1lib.actor.bomb import TNTSpawner +from os import listdir as ls + + +def error(real): # not fake + bui.getsound('error').play() + with ga().context: + img = bs.gettexture("ouyaAButton") + push(real, color=(1, 0, 0), top=Nice.top_msg, image=img) + + +def ding(fake): # fake + if Nice.do_ding: + bui.getsound('ding').play() + with ga().context: + img = bs.gettexture("ouyaOButton") + push(fake, color=(0, 1, 0), top=Nice.top_msg, image=img) + + +def var(s, v=None): + cfg = bui.app.config + if v is None: + try: + return cfg['sb_'+s] + except: + return 0 + else: + cfg['sb_'+s] = v + cfg.commit() + + +class Nice(igm): + # config, trash code ik + def_attrs = [False, "Spaz", 2.0, 0.0, 1.0, 0.4, (1, 1, 1), 3, "normal", False, False, + (1, 1, 1), 0.5, False, 0.0, False, False, 9.0, 5.0, 1.0, 0.7, True, False, + False, False, False, False, '$', (1, 1, 1)] + a = var('do_ding') + do_ding = a if isinstance(a, bool) else True + a = var('while_control') + while_control = a if isinstance(a, bool) else True + lite_mode = var('lite_mode') + animate_camera = var('animate_camera') + top_msg = var('top_msg') + notify_bot_ded = var('notify_bot_ded') + pause_when_bots = var('pause_when_bots') + drop_indox = 9 + drop_cords = (69123, 0, 0) + tweak_dux = None + tweak_2d = False + tweakz_dux = None + LTWAC = (0.9, 0.9, 0.9) + lmao_teams = [] + pending = [] # pending effects on bot from load_window + pending2 = [] # same but for drops + toxic_bots = [] + next_team_id = 2 + team_to_nuke = None + ga_tint = (1.30, 1.20, 1) + indox = 0 # spawn_window's character image order + val_attrs = def_attrs.copy() + + # my dumb positioning + soff = (0, 0) + howoff = 400 + _pos = 0 + _anim_out = 'out_right' + _anim_outv = 'out_left' + anim_in = 'in_right' + anim_inv = 'in_left' + center_pos = (0, 140) + scale = 1.3 + + # objects + node_gravity_scale = 1.0 + node_sticky = False + node_reflect = False + node_reflect2 = False + node_reflection_scale = [1.2] + brobordd = None + + def pause(s, b): + try: + ga().globalsnode.paused = b + except AttributeError: + pass # i am not the host + + def __init__(s, call_sand=True): + try: + for i in ga().players: + if i.sessionplayer.inputdevice.client_id == -1: + s.brobordd = i + break + except: + pass # not the host. + s.thex = 0.0 + s.they = 0.0 + s.spaz_not_fly = Spaz.on_jump_press + s.auto_spawn_on_random = False + s.random_peace = False + s._height = 300 + s._width = 500 + global root_widget, old_ga, nood + if Nice.pause_when_bots: + with ga().context: + bs.timer(0, bs.Call(s.pause, True)) + if str(ga()) != old_ga: + s.on_ga_change() + if call_sand: + root_widget = s._rw = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(s._width, s._height), + color=cola, + transition=s.anim_in, + stack_offset=s.soff, + scale=s.scale) + + def pos(): + s._pos = 0 if s._pos else 1 + if s._pos: + pos_left() + else: + pos_right() + + def pos_left(): + s._anim_out = 'out_left' + s._anim_outv = 'out_right' + s.anim_in = 'in_left' + s.anim_inv = 'in_right' + s.soff = (-s.howoff, 0) + bui.containerwidget(edit=s._rw, + transition=None, + position=(0, 150), + stack_offset=(-s.howoff, 0)) + bui.containerwidget(edit=s._rw, transition='in_left') + bui.buttonwidget(edit=s._LR, label='Right') + bui.buttonwidget(edit=s.center_btn, position=(395, 250)) + + def pos_right(): + s._anim_out = 'out_right' + s._anim_outv = 'out_left' + s.anim_in = 'in_right' + s.anim_inv = 'in_left' + s.soff = (s.howoff, 0) + bui.containerwidget(edit=s._rw, + transition=None, + position=(930, 140), + stack_offset=(s.howoff, 0)) + bui.containerwidget(edit=s._rw, transition='in_right') + bui.buttonwidget(edit=s._LR, label='Left') + bui.buttonwidget(edit=s.center_btn, position=(30, 250)) + + s._LR = bui.buttonwidget( + parent=root_widget, + size=(50, 15), + label='Left', + scale=s.scale, + button_type='square', + position=(395, 30), + color=colb, + textcolor=wht, + on_activate_call=bs.Call(pos)) + + bui.textwidget(parent=root_widget, + color=(0.1, 0.7, 1), + text='Sandbox', + position=(200, 250)) + + bui.buttonwidget(parent=root_widget, + label='Spawn', + size=(130, 50), + color=colb, + icon=bui.gettexture("cuteSpaz"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(40, 185), + on_activate_call=bs.Call(s.spawn_window)) + + bui.buttonwidget(parent=root_widget, + label='Control', + size=(130, 50), + color=colb, + icon=bui.gettexture("controllerIcon"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(180, 185), + on_activate_call=bs.Call(s.control_window)) + + bui.buttonwidget(parent=root_widget, + label='Tune', + color=colb, + icon=bui.gettexture("settingsIcon"), + iconscale=s.scale, + size=(130, 50), + textcolor=wht, + button_type='square', + position=(320, 185), + on_activate_call=bs.Call(s.config_window)) + + bui.buttonwidget(parent=root_widget, + label='Modify', + color=colb, + size=(130, 50), + icon=bui.gettexture("advancedIcon"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(40, 125), + on_activate_call=bs.Call(s.mod_window)) + + bui.buttonwidget(parent=root_widget, + label='Effect', + size=(130, 50), + color=colb, + icon=bui.gettexture("graphicsIcon"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(180, 125), + on_activate_call=bs.Call(s.effect_window)) + + bui.buttonwidget(parent=root_widget, + label='Listen', + size=(130, 50), + color=colb, + icon=bui.gettexture("audioIcon"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(320, 125), + on_activate_call=bs.Call(s.listen_window)) + + bui.buttonwidget(parent=root_widget, + label='Deploy', + size=(130, 50), + color=colb, + icon=bui.gettexture("star"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(40, 65), + on_activate_call=bs.Call(s.drop_window)) + + bui.buttonwidget(parent=root_widget, + label='Tweak', + size=(130, 50), + color=colb, + icon=bui.gettexture("menuIcon"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(180, 65), + on_activate_call=bs.Call(s.tweak_window)) + + bacc = bui.buttonwidget( + parent=root_widget, + size=(50, 15), + label='Back', + scale=s.scale, + button_type='square', + position=(30, 30), + color=colb, + textcolor=wht, + on_activate_call=bs.Call(s.back)) + bui.containerwidget(edit=root_widget, cancel_button=bacc) + + s.center_btn = bui.buttonwidget( + parent=root_widget, + size=(50, 15), + label='Center', + scale=s.scale, + button_type='square', + position=(30, 250), # (395, 250) if left + color=colb, + textcolor=wht, + on_activate_call=bs.Call(s.center)) + + bui.buttonwidget(parent=root_widget, + label='More', + size=(130, 50), + color=colb, + icon=bui.gettexture("storeIcon"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(320, 65), + on_activate_call=bs.Call(s.lol_window)) + + def center(s): + if s.soff == (0, 0): + error("Sandbox is already centered what are u doing") + return + s.soff = (0, 0) + bui.containerwidget(edit=s._rw, transition=None) + bui.containerwidget(edit=s._rw, position=(s.center_pos[0] + s.howoff, s.center_pos[1])) + bui.containerwidget(edit=s._rw, stack_offset=s.soff) + bui.containerwidget(edit=s._rw, transition='in_scale') + + def back(s, wosh=False): + s.kill(wosh, root_widget) + s.pause(False) + + def kill(s, wosh=False, who=None, keep_hl=False, anim=True, rev=False): + try: + bui.containerwidget(edit=who, transition=( + s._anim_out if not rev else s._anim_outv) if anim else None) + except: + pass + if wosh: + bui.getsound('swish').play() + if not keep_hl: + s.hl3(None, False) + + def lol_window(s): + if ga() is None: + push("no MORE for you bud,\nyou are not the host.", color=(1, 1, 0)) + return + s.lol_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 240), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=s.lol_widget, + color=(0.1, 0.7, 1), + text='More', + position=(210, 190)) + + bui.buttonwidget(parent=s.lol_widget, + label='Gather', + size=(130, 50), + color=colb, + icon=bui.gettexture("achievementTeamPlayer"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(40, 125), + on_activate_call=bs.Call(s.lol_teams_window)) + + bui.buttonwidget(parent=s.lol_widget, + label='Epic', + size=(130, 50), + color=colb, + icon=bui.gettexture("nextLevelIcon"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(180, 125), + on_activate_call=bs.Call(s.epic_window)) + + bui.buttonwidget(parent=s.lol_widget, + label='Tint', + size=(130, 50), + color=colb, + icon=bui.gettexture("achievementRunaround"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(320, 125), + on_activate_call=bs.Call(s.light_window)) + + bui.buttonwidget(parent=s.lol_widget, + label='Dim', + size=(130, 50), + color=colb, + icon=bui.gettexture("shadow"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(40, 65), + on_activate_call=bs.Call(s.dim_window)) + + bui.buttonwidget(parent=s.lol_widget, + label='Load', + size=(130, 50), + color=colb, + icon=bui.gettexture("inventoryIcon"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(180, 65), + on_activate_call=bs.Call(s.load_window)) + + bui.buttonwidget(parent=s.lol_widget, + label='About', + size=(130, 50), + color=colb, + icon=bui.gettexture("heart"), + iconscale=s.scale, + textcolor=wht, + button_type='square', + position=(320, 65), + on_activate_call=bs.Call(s.about_window)) + + bacc = bui.buttonwidget( + parent=s.lol_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.lol_widget)) + bui.containerwidget(edit=s.lol_widget, cancel_button=bacc) + + bui.textwidget(parent=s.lol_widget, + color=(0.7, 0.7, 0.7), + scale=s.scale/3, + text="* Not advanced enough? tweak 'globals' at Tweak menu,\n it holds the activity node which is basically everything.", + position=(180, 30)) + + def about_window(s): + about_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 450), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + txt = "SandBox v1.2 BETA\nThe mod which does almost everything in the game.\n\n" \ + "Made this mod for myself to test future mods freely, though you are\n" \ + "free to use it too!\n\nSorry if you found any bugs, I did my best to fix all existing bugs\n" \ + "and excepted a lot of lines, if I found more bugs I'm gonna fix them asap.\n\n" \ + "Coded using Galaxy A14 (4/64) using GNU Nano on Termux!" \ + "\n\nBig thanks to:\nYOU for trying this mod!" + + s.about_preview_text = bui.textwidget(parent=about_widget, + text=txt, + scale=s.scale, + maxwidth=450, + position=(30, 350)) + + bui.textwidget(parent=about_widget, + color=(0.1, 0.7, 1), + text='About', + position=(200, 400), + maxwidth=150) + + bacc = bui.buttonwidget( + parent=about_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, about_widget)) + bui.containerwidget(edit=about_widget, cancel_button=bacc) + + def load_window(s): + if Nice.pause_when_bots: + error("Cannot use Load Window while game is paused") + return + s.load_dux = None + load_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 300), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + s.load_preview_text = bui.textwidget(parent=load_widget, + text='', + size=(50, 50), + scale=s.scale/1.4, + maxwidth=200, + position=(280, 175)) + + bui.textwidget(parent=load_widget, + color=(0.1, 0.7, 1), + text='Load', + position=(200, 250), + maxwidth=150) + + bui.buttonwidget( + parent=load_widget, + size=(70, 30), + label='Load', + button_type='square', + scale=s.scale, + color=colb, + textcolor=wht, + position=(370, 30), + on_activate_call=bs.Call(s.do_load)) + + load_scroll = bui.scrollwidget(parent=load_widget, + position=(30, 80), + claims_up_down=False, + claims_left_right=True, + autoselect=True, + size=(250, 150)) + + load_sub = bui.containerwidget(parent=load_scroll, + background=False, + size=(190, len(load_name)*26), + color=(0.3, 0.3, 0.3), + scale=s.scale) + + bacc = bui.buttonwidget( + parent=load_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, load_widget)) + bui.containerwidget(edit=load_widget, cancel_button=bacc) + bui.textwidget(edit=s.load_preview_text, text="Preset Name") + + for i in range(len(load_name)): + j = len(load_name)-1-i + bui.textwidget(parent=load_sub, + scale=s.scale/2, + text=(load_name[j]), + h_align='left', + v_align='center', + color=(1, 1, 1), + on_activate_call=bs.Call(s.load_preview, j), + selectable=True, + autoselect=True, + click_activate=True, + size=(180, 29), + position=(-30, (20 * i))) + + def load_preview(s, i): + bui.textwidget(edit=s.load_preview_text, text=load_name[i]) + s.load_dux = i + + def do_load(s): + i = s.load_dux + if i is None: + error("Select a preset or get out.") + return + s.load_preset(i) + ding(f"Loaded '{load_name[i]}'") + + def load_preset(s, i): + if not i: # Beboo + Nice.indox = 9 + Nice.val_attrs[1] = "B-9000" + Nice.val_attrs[6] = (1, 0, 0) + Nice.val_attrs[7] = 0 + Nice.val_attrs[11] = (0.6, 0, 0) + Nice.val_attrs[12] = 0.6 # punchiness + Nice.val_attrs[13] = True + Nice.val_attrs[17] = 99 + Nice.val_attrs[18] = 0 + Nice.val_attrs[20] = 0 + Nice.val_attrs[22] = True + Nice.val_attrs[23] = True # host + Nice.val_attrs[24] = True + Nice.val_attrs[25] = True + Nice.val_attrs[26] = True # your bots + Nice.val_attrs[27] = "Beboo" + Nice.val_attrs[28] = (0.7, 0, 0) + Nice.pending = ["sp", "speed", "toxic", "constant_heal"] + s.spawn_window() + elif i == 1: # Kronk Buddy + Nice.indox = 1 + Nice.val_attrs[1] = "Kronk" + Nice.val_attrs[6] = (0, 1, 1) + Nice.val_attrs[7] = 0 + Nice.val_attrs[11] = (0, 0, 1) + Nice.val_attrs[12] = 0.6 + Nice.val_attrs[13] = True + Nice.val_attrs[17] = 99 + Nice.val_attrs[18] = 0 + Nice.val_attrs[20] = 0 + Nice.val_attrs[22] = True + Nice.val_attrs[24] = True + Nice.val_attrs[25] = True + Nice.val_attrs[27] = "Kronko Buddo" + Nice.val_attrs[28] = (0, 0, 0.7) + s.spawn_window() + elif i == 3: # Infinitely Cursed Jack + Nice.indox = 5 + Nice.val_attrs[0] = True # bouncy + Nice.val_attrs[1] = "Jack Morgan" + Nice.val_attrs[6] = (1, 1, 0) + Nice.val_attrs[7] = 0 + Nice.val_attrs[11] = (1, 1, 0) + Nice.val_attrs[12] = 0 + Nice.val_attrs[13] = True + Nice.val_attrs[17] = 99 + Nice.val_attrs[18] = 0 + Nice.val_attrs[20] = 0 + Nice.val_attrs[22] = False # start invincible + Nice.val_attrs[23] = True # host + Nice.val_attrs[24] = True # players + Nice.val_attrs[25] = True # bots + Nice.val_attrs[26] = True # your bots + Nice.val_attrs[27] = "Jackie KMS" + Nice.val_attrs[28] = (1, 1, 0) + Nice.pending = ["infinite_curse"] + s.spawn_window() + elif i == 2: # Flying Pixel + Nice.indox = 10 + Nice.val_attrs[0] = True # bouncy + Nice.val_attrs[1] = "Pixel" + Nice.val_attrs[6] = (1, 0, 1) + Nice.val_attrs[7] = 0 + Nice.val_attrs[11] = (1, 0, 1) + Nice.val_attrs[12] = 0 + Nice.val_attrs[13] = True + Nice.val_attrs[17] = 99 + Nice.val_attrs[18] = 0 + Nice.val_attrs[20] = 0 + Nice.val_attrs[22] = True # start invincible + Nice.val_attrs[23] = True # host + Nice.val_attrs[24] = True # players + Nice.val_attrs[25] = True # bots + Nice.val_attrs[26] = True # your bots + Nice.val_attrs[27] = "Pixie" + Nice.val_attrs[28] = (1, 0, 1) + Nice.pending = ["constant_jump", "constant_heal"] + s.spawn_window() + elif i == 4: # Big Shiny TNT + Nice.pending2 = ["big_bomb"] + Nice.drop_indox = 9 + s.drop_window() + elif i == 5: # Long fuse bomb + Nice.pending2 = ["long_fuse"] + Nice.drop_indox = 17 + s.drop_window() + elif i == 6: # Huge safe mine + Nice.pending2 = ["big_bomb"] + Nice.drop_indox = 10 + s.drop_window() + + def dim_window(s): + s.dim_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 250), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + + bui.buttonwidget(parent=s.dim_widget, + size=(200, 50), + label="Inner", + textcolor=wht, + scale=s.scale, + color=colb, + position=(20, 125), + on_activate_call=s.switch_dim) + + bui.textwidget(parent=s.dim_widget, + color=(0.1, 0.7, 1), + text='Which Vignette?', + scale=s.scale, + h_align='center', + v_align='center', + position=(125, 200)) + + bui.buttonwidget(parent=s.dim_widget, + size=(200, 50), + label="Outer", + scale=s.scale, + color=colb, + textcolor=wht, + position=(20, 60), + on_activate_call=bs.Call(s.switch_dim, 1)) + + bacc = bui.buttonwidget( + parent=s.dim_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.dim_widget)) + bui.containerwidget(edit=s.dim_widget, cancel_button=bacc) + + def switch_dim(s, t=0): + s.kill(True, s.dim_widget) + title = "Outer" if t else "Inner" + title = title+" Vignette" + s.switch_dim_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 250), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=s.switch_dim_widget, + color=(0.1, 0.7, 1), + text=title, + scale=s.scale, + h_align='center', + v_align='center', + position=(125, 200)) + + p = ga().globalsnode + p = p.vignette_outer if t else p.vignette_inner + x = bui.textwidget( + parent=s.switch_dim_widget, + text=str(p[0])[:5], + editable=True, + size=(200, 25), + h_align='center', + v_align='center', + position=(55, 150)) + y = bui.textwidget( + parent=s.switch_dim_widget, + size=(200, 25), + text=str(p[1])[:5], + editable=True, + h_align='center', + v_align='center', + position=(55, 120)) + z = bui.textwidget( + parent=s.switch_dim_widget, + size=(200, 25), + text=str(p[2])[:5], + editable=True, + h_align='center', + v_align='center', + position=(55, 90)) + + bui.buttonwidget( + parent=s.switch_dim_widget, + size=(60, 20), + label='Set', + scale=s.scale, + color=colb, + textcolor=wht, + position=(200, 30), + on_activate_call=bs.Call(s.collect_dim, x, y, z, t)) + + def back(s): + s.kill(True, s.switch_dim_widget, keep_hl=True) + s.dim_window() + bacc = bui.buttonwidget( + parent=s.switch_dim_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(back, s)) + bui.containerwidget(edit=s.switch_dim_widget, cancel_button=bacc) + + def gettext(s, w): + return cast(str, bui.textwidget(query=w)) + + def collect_dim(s, x, y, z, outer): + n = ga().globalsnode + t1 = s.gettext(x) + t2 = s.gettext(y) + t3 = s.gettext(z) + emp = "X" if not t1 else "Y" if not t2 else "Z" if not z else None + if emp: + error(f"{emp} value cannot be empty!") + return + try: + v = eval(f"({t1}, {t2}, {t3})") + except Exception as e: + error(str(e)) + return + if outer: + n.vignette_outer = v + else: + n.vignette_inner = v + s.kill(True, s.switch_dim_widget) + ding("Dim updated!") + + def epic_window(s): + s.epic_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 200), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=s.epic_widget, + color=(0.1, 0.7, 1), + text='Epic', + scale=s.scale, + h_align='center', + v_align='center', + position=(125, 150)) + + s.epic_pick = bui.buttonwidget(parent=s.epic_widget, + size=(200, 50), + label="Make Fast" if ga().globalsnode.slow_motion else "Make Epic", + textcolor=wht, + color=cola, + scale=s.scale, + position=(20, 75), + on_activate_call=s.switch_epic) + + bacc = bui.buttonwidget( + parent=s.epic_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.epic_widget)) + bui.containerwidget(edit=s.epic_widget, cancel_button=bacc) + + def switch_epic(s): + b = not ga().globalsnode.slow_motion + ga().globalsnode.slow_motion = b + s.do_your_thing(b) + bui.buttonwidget(edit=s.epic_pick, label=("Make Fast" if b else "Make Epic")) + + def light_window(s): + s.light_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 250), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=s.light_widget, + color=(0.1, 0.7, 1), + text='Tint', + scale=s.scale, + h_align='center', + v_align='center', + position=(125, 200)) + + global light_pick + tent = ga().globalsnode.tint + ntent = s.negate(tent) + light_pick = bui.buttonwidget(parent=s.light_widget, + size=(200, 50), + label="Change Color", + textcolor=wht, + scale=s.scale, + position=(20, 125), + on_activate_call=bs.Call(PickerLight, tent)) + bui.buttonwidget(edit=light_pick, color=tent, textcolor=ntent) + + bui.buttonwidget( + parent=s.light_widget, + size=(60, 20), + label='/ 1.1', + scale=s.scale, + color=colb, + textcolor=wht, + position=(200, 70), + on_activate_call=bs.Call(s.mult)) + + bui.buttonwidget( + parent=s.light_widget, + size=(60, 20), + label='x 1.1', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 70), + on_activate_call=bs.Call(s.mult, 1)) + + bui.buttonwidget( + parent=s.light_widget, + size=(60, 20), + label='Set', + scale=s.scale, + color=colb, + textcolor=wht, + position=(200, 30), + on_activate_call=bs.Call(s.collect_light)) + + bacc = bui.buttonwidget( + parent=s.light_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.light_widget)) + bui.containerwidget(edit=s.light_widget, cancel_button=bacc) + + def mult(s, i=0): + c = Nice.ga_tint + x = 1.1 if i else (1/(1.1)) + Nice.ga_tint = c = (c[0]*x, c[1]*x, c[2]*x) + bui.buttonwidget(edit=light_pick, color=c) + bui.buttonwidget(edit=light_pick, textcolor=Nice.negate(Nice, c)) + bui.buttonwidget(edit=light_pick, on_activate_call=bs.Call(PickerLight, c)) + + def collect_light(s): + ding("Success!") + s.kill(True, s.light_widget) + ga().globalsnode.tint = Nice.ga_tint + + def lol_teams_window(s): + s.LTW = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 250), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + bui.buttonwidget(parent=s.LTW, + size=(200, 50), + label="Add", + textcolor=wht, + scale=s.scale, + color=colb, + icon=bui.gettexture("powerupHealth"), + position=(20, 125), + on_activate_call=s.lol_teams_window_add) + + bui.textwidget(parent=s.LTW, + color=(0.1, 0.7, 1), + text='What To Do?', + scale=s.scale, + h_align='center', + v_align='center', + position=(125, 200)) + + bui.buttonwidget(parent=s.LTW, + size=(200, 50), + label="Nuke", + scale=s.scale, + color=colb, + icon=bui.gettexture("powerupCurse"), + textcolor=wht, + position=(20, 60), + on_activate_call=bs.Call(s.lol_teams_window_nuke)) + + bacc = bui.buttonwidget( + parent=s.LTW, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.LTW)) + bui.containerwidget(edit=s.LTW, cancel_button=bacc) + + def lol_teams_window_nuke(s): + s.LTWN = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 300), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=s.LTWN, + color=(0.1, 0.7, 1), + text='Nuke', + position=(200, 250), + maxwidth=250) + + bui.buttonwidget( + parent=s.LTWN, + size=(70, 30), + label='Nuke', + button_type='square', + scale=s.scale, + color=colb, + textcolor=wht, + position=(370, 30), + on_activate_call=bs.Call(s.do_nuke)) + + LTWNS = bui.scrollwidget(parent=s.LTWN, + position=(30, 80), + claims_up_down=False, + claims_left_right=True, + autoselect=True, + size=(300, 150)) + + s.LTWN_sub = bui.containerwidget(parent=LTWNS, + background=False, + size=(300, len(Nice.lmao_teams)*26), + color=(0.3, 0.3, 0.3), + scale=s.scale) + s.LTWNP = bui.textwidget(parent=s.LTWN, + text='Team Name', + size=(50, 50), + scale=s.scale/1.4, + maxwidth=115, + color=wht, + position=(340, 175)) + + bacc = bui.buttonwidget( + parent=s.LTWN, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.LTWN)) + bui.containerwidget(edit=s.LTWN, cancel_button=bacc) + + s.LTWN_load_teams() + + def LTWN_load_teams(s): + for w in s.LTWN_sub.get_children(): + w.delete() + for i in range(len(Nice.lmao_teams)): + i = len(Nice.lmao_teams)-1-i + t = Nice.lmao_teams[i] + bui.textwidget(parent=s.LTWN_sub, + scale=s.scale/2, + text=t.name, + h_align='left', + v_align='center', + color=t.color, + on_activate_call=bs.Call(s.LTWN_prev, i), + selectable=True, + autoselect=True, + click_activate=True, + size=(180, 29), + position=(-30, (20 * i))) + + def LTWN_prev(s, i): + t = Nice.lmao_teams[i] + s.team_to_nuke = t + bui.textwidget(edit=s.LTWNP, text=t.name, color=t.color) + + def do_nuke(s): + if len(Nice.lmao_teams) < 1: + error("Remove what u blind?") + return + if s.team_to_nuke is None: + error("Select a team") + return + t = s.team_to_nuke + s.team_to_nuke = None + ga().remove_team(t) + ding(f"Removed '{t.name}'") + Nice.lmao_teams.remove(t) + bui.containerwidget(edit=s.LTWN_sub, size=(300, len(Nice.lmao_teams)*26)) + s.LTWN_load_teams() + + def lol_teams_window_add(s): + s.LTWA = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(450, 230), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + bacc = bui.buttonwidget( + parent=s.LTWA, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.LTWA)) + bui.containerwidget(edit=s.LTWA, cancel_button=bacc) + + bui.textwidget(parent=s.LTWA, + color=(0.1, 0.7, 1), + text='Add', + position=(200, 180)) + + bui.textwidget(parent=s.LTWA, + color=wht, + text="Team Name:", + scale=s.scale/1.6, + position=(30, 160), + h_align="left", + maxwidth=400) + + s.LTWA_box = bui.textwidget(parent=s.LTWA, + editable=True, + description="Enter team name:", + position=(30, 130), + size=(400, 30), + h_align="left", + maxwidth=400) + + bui.textwidget(parent=s.LTWA, + color=wht, + text="Team Color:", + scale=s.scale/1.6, + position=(30, 100), + h_align="left", + maxwidth=400) + + global LTWAB + LTWAB = bui.buttonwidget( + parent=s.LTWA, + size=(70, 30), + label='Pick', + button_type="square", + textcolor=s.negate(Nice.LTWAC), + position=(30, 70), + color=Nice.LTWAC, + on_activate_call=bs.Call(PickerLol, Nice.LTWAC)) + + s.LTWA_ran = bui.buttonwidget( + parent=s.LTWA, + size=(60, 20), + color=cola, + scale=s.scale, + label='Random', + textcolor=wht, + button_type="square", + position=(340, 70), + on_activate_call=bs.Call(s.LTWA_random)) + + bui.buttonwidget( + parent=s.LTWA, + size=(60, 20), + label='Done', + scale=s.scale, + color=colb, + textcolor=wht, + position=(340, 30), + on_activate_call=bs.Call(s.lol_teams_window_done)) + + def negate(s, c): return (1-c[0], 1-c[1], 1-c[2]) + + def LTWA_random(s): + global LTWAB + r = random.choice(random_team) + Nice.LTWAC = r[1] + bui.textwidget(edit=s.LTWA_box, text=r[0]) + bui.buttonwidget(edit=LTWAB, color=r[1], textcolor=s.negate(r[1])) + + def lol_teams_window_done(s): + LTN = cast(str, bui.textwidget(query=s.LTWA_box)) + if not LTN: + error("Enter a team name or just leave a space there") + return + team = bs.SessionTeam(Nice.next_team_id, name=LTN, color=Nice.LTWAC) + Nice.lmao_teams.append(team) + ga().add_team(team) + Nice.next_team_id += 1 + s.kill(True, s.LTWA) + ding(f"'{LTN}' was added!") + + def tweak_window(s): + if ga() is None: + push("You don't meet the minimum requirements\nto use BorddTweaker: BEING THE HOST.", color=(1, 1, 0)) + return + p = s.brobordd.node.position + with ga().context: + nuds = bs.getnodes() + s.nodes = [] + s.nodes_2d = [] + # fill up both 3D and 2D nodes + for i in nuds: + try: + pos = i.position + except: + continue + try: + obj = i.getdelegate(object) + except: + obj = None + if len(pos) == 3: + s.nodes.append([]) + s.nodes[-1].append(pos) + s.nodes[-1].append(i), s.nodes[-1].append(obj) + elif len(pos) == 2: + s.nodes_2d.append([]) + s.nodes_2d[-1].append(pos) + s.nodes_2d[-1].append(i) + s.nodes_2d[-1].append(obj) + s.nodes.append([]) + s.nodes[-1].append((0, 0, 0)) + s.nodes[-1].append(ga().globalsnode), s.nodes[-1].append(None) + # sort by closest (3D only) + s.nodes = sorted(s.nodes, key=lambda k: math.dist(p, k[0])) + # fill up 3D names and pics + s.tweak_name = [] + s.tweak_texture = [] + for n in range(len(s.nodes)): + obj = " ~" if s.nodes[n][2] is not None else "" + try: + s.tweak_name.append(str(s.nodes[n][1].getnodetype())+f" {n}"+obj) + except: + s.tweak_name.append(f"idk {n}"+obj) + try: + t = str(s.nodes[n][1].color_texture) + on = t.find('"') + off = t.find('"', on+1) + s.tweak_texture.append(bui.gettexture(t[on+1:off])) + except: + try: + t = str(s.nodes[n][1].texture) + on = t.find('"') + off = t.find('"', on+1) + s.tweak_texture.append(bui.gettexture(t[on+1:off])) + except: + try: + s.tweak_texture.append(str(s.nodes[n][1].text)) + except: + s.tweak_texture.append(bui.gettexture("tv")) + try: + thing = s.what_is(s.nodes[n][1].mesh) + except: + continue + s.tweak_name[-1] = s.tweak_name[-1]+thing + # fill up 2D names and pics too + s.tweak_name_2d = [] + s.tweak_texture_2d = [] + for n in range(len(s.nodes_2d)): + obj = " ~" if s.nodes_2d[n][2] is not None else "" + try: + s.tweak_name_2d.append(str(s.nodes_2d[n][1].getnodetype())+f" {n}"+obj) + except: + s.tweak_name_2d.append(f"idk {n}"+obj) + try: + t = str(s.nodes_2d[n][1].color_texture) + on = t.find('"') + off = t.find('"', on+1) + s.tweak_texture_2d.append(bui.gettexture(t[on+1:off])) + except: + try: + t = str(s.nodes_2d[n][1].texture) + on = t.find('"') + off = t.find('"', on+1) + s.tweak_texture_2d.append(bui.gettexture(t[on+1:off])) + except: + try: + s.tweak_texture_2d.append(str(s.nodes_2d[n][1].text)) + except: + s.tweak_texture_2d.append(bui.gettexture("tv")) + try: + thing = s.what_is(s.nodes_2d[n][1].mesh) + except: + continue + s.tweak_name_2d[-1] = s.tweak_name_2d[-1]+thing + + s.tweak_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 300), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + s.tweak_preview_image = bui.buttonwidget(parent=s.tweak_widget, + label='', + size=(50, 50), + position=(300, 175), + button_type='square', + color=colb, + mask_texture=bui.gettexture('characterIconMask')) + + s.tweak_preview_text = bui.textwidget(parent=s.tweak_widget, + text='', + size=(50, 50), + scale=s.scale/1.4, + maxwidth=115, + position=(365, 175)) + + s.tweak_preview_text2 = bui.textwidget(parent=s.tweak_widget, + text='', + size=(50, 50), + scale=s.scale/1.8, + maxwidth=115, + position=(360, 155)) + + bui.textwidget(parent=s.tweak_widget, + color=(0.1, 0.7, 1), + text='Tweak', + position=(300, 240), + maxwidth=150) + + bui.buttonwidget( + parent=s.tweak_widget, + size=(70, 30), + label='Select', + button_type='square', + scale=s.scale, + color=colb, + textcolor=wht, + position=(370, 30), + on_activate_call=bs.Call(s.do_tweak)) + + tweak_scroll = bui.scrollwidget(parent=s.tweak_widget, + position=(30, 80), + claims_up_down=False, + claims_left_right=True, + autoselect=True, + size=(250, 150)) + + s.tweak_sub = bui.containerwidget(parent=tweak_scroll, + background=False, + color=(0.3, 0.3, 0.3), + scale=s.scale) + + tabdefs = [('3d', '3D Nodes'), ('2d', "2D Nodes")] + + s.tweak_tab = TabRow( + s.tweak_widget, + tabdefs, + pos=(30, 230), + size=(250, 0), + on_select_call=s.switch_tweak_tab) + + # the right order + s.tweak_tab.update_appearance('3d') + s.cola_fill(s.tweak_widget) + s.load_tweak_nodes() + + bacc = bui.buttonwidget( + parent=s.tweak_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.tweak_widget)) + bui.containerwidget(edit=s.tweak_widget, cancel_button=bacc) + + bui.textwidget( + parent=s.tweak_widget, + size=(60, 20), + text='~ has an object, 3d sorted by how close node is.', + scale=s.scale/2, + color=wht, + maxwidth=250, + position=(19, 60)) + + def what_is(s, t): + t = str(t).split('"')[1] + for i in what_is_arr: + if i[0] == t: + return f" ({i[1]})" + return "" + + def switch_tweak_tab(s, t): + s.tweak_tab.update_appearance(t) + s.cola_fill(s.tweak_widget) + s.load_tweak_nodes(t == '3d') + + def load_tweak_nodes(s, t=True): + # selected index is s.tweak_dux + tn = s.tweak_name if t else s.tweak_name_2d + alex = s.nodes if t else s.nodes_2d + # clean up + for c in s.tweak_sub.get_children(): + c.delete() + bui.textwidget(edit=s.tweak_preview_text, text="Node Type") + bui.buttonwidget(edit=s.tweak_preview_image, texture=bui.gettexture("tv"), color=(1, 1, 1)) + bui.containerwidget(edit=s.tweak_sub, size=(190, len(alex)*25.6)) + s.tweak_dux = None + s.tweak_2d = False + + for i in range(len(alex)): + bui.textwidget(parent=s.tweak_sub, + scale=s.scale/2, + text=(tn[i]), + h_align='left', + v_align='center', + color=(1, 1, 1), + on_activate_call=bs.Call(s.tweak_preview, i, t), + selectable=True, + autoselect=True, + click_activate=True, + size=(180, 29), + position=(-30, (20 * len(alex)) - (20 * i) - 30)) + + def tweak_preview(s, i, b): + s.tweak_dux = i + s.tweak_2d = not b + tn = s.tweak_name if b else s.tweak_name_2d + tt = s.tweak_texture if b else s.tweak_texture_2d +# nud = s.nodes[i] if b else s.nodes_2d[i] +# try: s.draw_locator(nud[1].position) +# except Exception as e: error(str(e)) + bui.textwidget(edit=s.tweak_preview_text, text=tn[s.tweak_dux]) + k = tt[s.tweak_dux] + bui.textwidget(edit=s.tweak_preview_text2, text="") + if isinstance(k, str): + bui.textwidget(edit=s.tweak_preview_text2, + text=tt[s.tweak_dux], color=s.get_type_color("str")) + bui.buttonwidget(edit=s.tweak_preview_image, + texture=bui.gettexture("tv"), color=(1, 1, 1)) + else: + bui.buttonwidget(edit=s.tweak_preview_image, texture=tt[s.tweak_dux], color=(1, 1, 1)) + + def connect_dots(s, pos1, pos2): + spacing = 5 + x1, y1 = pos1 + x2, y2 = pos2 + # Calculate the distance between pos1 and pos2 + distance = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 + # Calculate the number of dots needed + num_dots = int(distance / spacing) + # Calculate the step for each coordinate + x_step = (x2 - x1) / num_dots + y_step = (y2 - y1) / num_dots + # Generate the dots + dots = [] + for i in range(num_dots): + dot_x = x1 + i * x_step + dot_y = y1 + i * y_step + dots.append((dot_x, dot_y)) + return dots + + def draw_char(s, char, pos, color=(0, 1, 1)): + with ga().context: + n = bs.newnode("text", attrs={ + "text": char, + "flatness": 1.0, + "scale": 1, + "position": pos, + "color": color + }) + s.royna.append(n) + return n + + def draw_locator(s, pos, pos2=(0, 0)): + try: + for node in s.royna: + node.delete() + except: + pass + s.royna = [] + dots = s.connect_dots(pos, pos2) + s.draw_char(char="B", pos=pos, color=(1, 0, 0)) + s.draw_char(char="A", pos=pos2, color=(1, 0, 0)) + for i in range(len(dots)): + n = s.draw_char(char="O", pos=dots[i]) + s.royna.append(n) + + def do_tweak(s): + b = s.tweak_2d + i = s.tweak_dux + try: + node = s.nodes_2d[i] if b else s.nodes[i] # list of 3 + except TypeError: + error("Select a node dum dum") + return + name = s.tweak_name_2d[i] if b else s.tweak_name[i] + s.tweakz_current_name = name + s.tweakz_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 300), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + s.tweakz_preview_text = bui.textwidget(parent=s.tweakz_widget, + text='', + size=(50, 50), + scale=s.scale/1.4, + maxwidth=170, + position=(290, 175)) + + s.tweakz_preview_text2 = bui.textwidget(parent=s.tweakz_widget, + text='', + size=(50, 50), + scale=s.scale/1.8, + maxwidth=170, + position=(290, 150)) + + bui.textwidget(parent=s.tweakz_widget, + color=(0.1, 0.7, 1), + text=f'Tweak {name}', + position=(300, 240), + maxwidth=150) + + bui.buttonwidget( + parent=s.tweakz_widget, + size=(70, 30), + label='Tweak', + button_type='square', + scale=s.scale, + color=colb, + textcolor=wht, + position=(370, 30), + on_activate_call=s.tweak_this) + + bui.buttonwidget( + parent=s.tweakz_widget, + size=(70, 30), + label='Call', + button_type='square', + scale=s.scale, + color=colb, + textcolor=wht, + position=(260, 30), + on_activate_call=s.call_this) + + tweakz_scroll = bui.scrollwidget(parent=s.tweakz_widget, + position=(30, 80), + claims_up_down=False, + claims_left_right=True, + autoselect=True, + size=(250, 150)) + + s.tweakz_sub = bui.containerwidget(parent=tweakz_scroll, + background=False, + color=(0.3, 0.3, 0.3), + scale=s.scale) + + tabdefs = [('node', 'Node'), ('obj', "Object")] + + s.tweakz_tab = TabRow( + s.tweakz_widget, + tabdefs, + pos=(30, 230), + size=(250, 0), + on_select_call=bs.Call(s.switch_tweakz_tab, node)) + + # the right order + s.tweakz_tab.update_appearance('node') + s.cola_fill(s.tweakz_widget) + s.load_tweakz_nodes(node) + + bacc = bui.buttonwidget( + parent=s.tweakz_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.tweakz_widget)) + bui.containerwidget(edit=s.tweakz_widget, cancel_button=bacc) + + def switch_tweakz_tab(s, node, t): + s.tweakz_tab.update_appearance(t) + s.cola_fill(s.tweakz_widget) + s.load_tweakz_nodes(node, t == 'node') + bui.textwidget(edit=s.tweak_preview_text2, text="") + + def load_tweakz_nodes(s, node, t=True): + # selected index is s.tweakz_dux + alex = node + s.tweakz_current_node = node + s.tweakz_is_node = t + tn = [] + typez = [] + value = [] + col = [] + blex = alex[1 if t else 2] + for i in dir(blex): + tn.append(i) + try: + attr = getattr(blex, i) if i not in [ + # gay + "punch_position", "punch_velocity", "punch_momentum_linear"] else (0, 0, 0) + except: + attr = None + typez.append(str(type(attr).__name__)) + value.append(attr) + if alex[2] is None and not t: + tn.append("No object lol") + s.tweakz_name = tn + s.tweakz_type = typez + s.tweakz_value = value + # clean up + for c in s.tweakz_sub.get_children(): + c.delete() + bui.textwidget(edit=s.tweakz_preview_text, text="Attribute") + bui.textwidget(edit=s.tweakz_preview_text2, text="Type") + bui.containerwidget(edit=s.tweakz_sub, size=(190, len(tn)*25.9)) + s.tweakz_dux = None + for i in range(len(typez)): + t = typez[i] + col.append(s.get_type_color(t)) + col.append((0.1, 0.1, 1)) + typez.append("byBordd") + + for i in range(len(tn)): + bui.textwidget(parent=s.tweakz_sub, + scale=s.scale/2, + text=(tn[i]), + color=col[i], + h_align='left', + v_align='center', + on_activate_call=bs.Call(s.tweakz_preview, i, t), + selectable=True, + autoselect=True, + click_activate=True, + size=(210, 29), + position=(-30, (20 * len(tn)) - (20 * i) - 30)) + + def get_type_color(s, t): + c = (1, 0.5, 0) # unknown orange + if t == "str": + c = (0, 0.6, 0) # green + elif t == "float": + c = (0, 1, 1) # cyan + elif t == "tuple": + c = (1, 0.6, 1) # light pink + elif t == "bool": + c = (1, 1, 0) # yellow + elif t == "NoneType": + c = (0.4, 0.4, 0.4) # grey + elif t == "Texture": + c = (0.6, 0, 0.8) # purple + return c + + def tweakz_preview(s, i, b): + s.tweakz_dux = i + tn = s.tweakz_name + typez = s.tweakz_type + bui.textwidget(edit=s.tweakz_preview_text, text=tn[i]) + bui.textwidget(edit=s.tweakz_preview_text2, text=typez[i]) + bui.textwidget(edit=s.tweakz_preview_text2, color=s.get_type_color(typez[i])) + + def tweak_this(s): + i = s.tweakz_dux + mode = 0 + try: + name = s.tweakz_current_name.split(" ")[0]+"."+s.tweakz_name[i] + except TypeError: + error("Select an attribute bruh") + return + try: + value = s.tweakz_value[i] + except IndexError: + error("Tweak no object? are you high?") + return + typ = s.tweakz_type[i] + if typ == "NoneType": + error("Can't modify NoneType,\nidk what type should it be.") + return + if str(value).startswith("<"): + if not typ == "Texture": + error(f"{name} is not tweakable!") + return + mode = 1 # texture picker + if typ == "tuple" and str(value).startswith("(<"): + error("This tuple is not tweakable!") + return + s.TTW = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(450, 200), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=s.TTW, + color=(0.1, 0.7, 1), + text=f"Tweak {name}", + position=(205, 150), + h_align="center", + maxwidth=400) + + bui.textwidget(parent=s.TTW, + color=s.get_type_color(typ), + text=typ, + scale=s.scale/1.6, + position=(205, 127), + h_align="center", + maxwidth=400) + + bui.textwidget(parent=s.TTW, + color=wht, + text="Default old value since the tweak window was opened:", + scale=s.scale/1.6, + position=(205, 100), + h_align="center", + maxwidth=400) + + if not mode: + s.tweakz_box = bui.textwidget(parent=s.TTW, + text=str(value), + editable=True, + position=(30, 75), + size=(400, 30), + h_align="center", + maxwidth=400) + elif mode == 1: + global THE_TB + s.tweakz_box = THE_TB = bui.textwidget(parent=s.TTW, + text=str(value).split('"')[1], + position=(30, 75), + size=(400, 30), + editable=True, + h_align="center", + maxwidth=400) + bui.buttonwidget(parent=s.TTW, + label="Pick Texture", + color=cola, + textcolor=wht, + position=(150, 35), + size=(150, 30), + on_activate_call=TexturePicker) + bui.textwidget(edit=s.tweakz_box, color=s.get_type_color(typ)) + + bui.buttonwidget( + parent=s.TTW, + size=(60, 20), + label='Cancel', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.TTW)) + + bui.buttonwidget( + parent=s.TTW, + size=(60, 20), + label='Set', + scale=s.scale, + color=colb, + textcolor=wht, + position=(340, 30), + on_activate_call=bs.Call(s.gather_tweakz, s.tweakz_name[i], typ, mode)) + + def gather_tweakz(s, name, typ, mode): + value = cast(str, bui.textwidget(query=s.tweakz_box)) + v = None # parsed value + node = s.tweakz_current_node + if not value: + error("If you won't enter something THEN U CAN PRESS CANCEL.") + # my nice yet stupid validator + elif typ == "bool" and value.lower() in ["true", "false"]: + v = value.lower() == 'true' + elif typ == "bool": + error("bool must be True or False, not '{}'".format(value)) + return + elif typ == "float": + try: + v = float(value) + except: + error("float must be a number (decimal), not '{}'".format(value)) + return + elif typ == "int": + try: + v = int(value) + except: + error("int must be a number (no decimals), not '{}'".format(value)) + return + elif typ == "tuple": + try: + e = eval(value) + v = e if type(e) is tuple else bro + except: + error( + f"tuple must be a goddamn tuple, not '{value}'\nlike this: (1.23, 1.91, 0.69)") + return + elif typ == "str": + v = value # string anything u like + elif typ == "Texture": + if value not in all_texture: + error(f"Unknown texture '{value}',\nuse 'white', 'black' or 'null' for empty ones") + return + with ga().context: + v = bs.gettexture(value) + # apply value to node + try: + with ga().context: + setattr(node[1 if s.tweakz_is_node else 2], name, v) + except Exception as e: + error(str(e) if str(e).strip() else f"No error info, repr(e):\n{repr(e)}") + else: + ding(f"Tweaked!") + s.kill(True, s.TTW) + + def call_this(s): + i = s.tweakz_dux + try: + name = s.tweakz_name[s.tweakz_dux] + except TypeError: + error("You better call a doctor instead,\nno attribute is selected") + return + try: + attr = getattr(s.tweakz_current_node[1 if s.tweakz_is_node else 2], name) + except AttributeError as e: + error("Node no longer exists\nwhat are you doing here?" if "No object lol" not in str( + e) else "Sure, equip a brain first") + return + s.CTW = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(450, 200), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=s.CTW, + color=(0.1, 0.7, 1), + text=f"Call {name}", + position=(205, 150), + h_align="center", + maxwidth=400) + + bui.buttonwidget( + parent=s.CTW, + size=(60, 20), + label='Cancel', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.CTW)) + + bui.buttonwidget( + parent=s.CTW, + size=(60, 20), + label='Call', + scale=s.scale, + color=colb, + textcolor=wht, + position=(340, 30), + on_activate_call=bs.Call(s.do_call_this, attr, name)) + + s.call_this_box = bui.textwidget(parent=s.CTW, + color=(0.1, 0.7, 1), + text="", + description="Leave blank to call with no args, args example:\n14.2, True, 'Yes', ...\nenter", + editable=True, + position=(30, 75), + size=(400, 30), + h_align="center", + maxwidth=400) + + bui.textwidget(parent=s.CTW, + color=wht, + text="Enter arguments separated by a comma (optional):", + scale=s.scale/1.6, + position=(205, 100), + h_align="center", + maxwidth=400) + + def do_call_this(s, attr, name): + t = cast(str, bui.textwidget(query=s.call_this_box)) + if t != "": + args = t.split(",") + try: + args = [eval(a.strip()) for a in args] + except Exception as e: + error(str(e)) + return + else: + args = [] + try: + with ga().context: + out = attr(*args) + ding( + f"Success! calling '{name}' (dumped to terminal)\nwith arguments {args}\noutputted: {out}") + s.kill(True, s.CTW) + except Exception as e: + error(str(e) if str(e).strip() else f"No error info, repr(e):\n{repr(e)}") + else: + print(f"SandBox.ByBordd: calling '{name}' outputted:\n{out}") + + def drop_window(s): + if ga() is None: + push("Drop? looks like you dropped your brain somewhere,\nyou are not the host.", color=(1, 1, 0)) + return + s.drop_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 300), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=s.drop_widget, + color=(0.1, 0.7, 1), + text='Deploy', + position=(210, 250), + maxwidth=250) + + Nice.drop_view = bui.buttonwidget(parent=s.drop_widget, + label='', + size=(100, 100), + position=(40, 120), + button_type='square', + color=(1, 1, 1), + texture=bui.gettexture(drop_texture[Nice.drop_indox]), + mask_texture=bui.gettexture('characterIconMask'), + on_activate_call=bs.Call(Picker, 69)) + + s.drop_where = bui.buttonwidget(parent=s.drop_widget, + label='', # Where To Deploy? + color=cola, + textcolor=wht, + size=(150, 100), + position=(170, 120), + button_type='square', + on_activate_call=s.where_to_drop) + s.update_cords_view(69) + + bui.buttonwidget(parent=s.drop_widget, + label='Edit\nAttrs', + color=cola, + textcolor=wht, + size=(100, 100), + position=(350, 120), + button_type='square', + on_activate_call=s.edit_drop_attrs) + + bui.buttonwidget(parent=s.drop_widget, + label='Locate position', + size=(120, 25), + position=(180, 85), + color=colb, + textcolor=wht, + button_type='square', + on_activate_call=bs.Call(s.show_in_game, 0, 69)) + + bui.buttonwidget(parent=s.drop_widget, + label='Draw a line', + size=(120, 25), + position=(180, 50), + button_type='square', + color=colb, + textcolor=wht, + on_activate_call=bs.Call(s.show_in_game, 1, 69)) + + Nice.drop_name = bui.textwidget(parent=s.drop_widget, + text=drop_name[Nice.drop_indox], + h_align='center', + v_align='center', + position=(65, 85)) + + def back(): s.kill(True, s.drop_widget, True); Nice.pending2 = [] + bacc = bui.buttonwidget( + parent=s.drop_widget, + size=(60, 20), + label='Back', + textcolor=wht, + scale=s.scale, + color=colb, + position=(30, 30), + on_activate_call=back) + bui.containerwidget(edit=s.drop_widget, cancel_button=bacc) + + bui.buttonwidget( + parent=s.drop_widget, + size=(100, 40), + label='Drop', + color=colb, + scale=s.scale, + textcolor=wht, + position=(350, 30), + on_activate_call=bs.Call(s.do_drop)) + + def edit_drop_attrs(s): + s.edit_drop_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 350), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=s.edit_drop_widget, + color=(0.1, 0.7, 1), + text='Edit Attributes', + position=(70, 300), + maxwidth=250) + + bui.textwidget(parent=s.edit_drop_widget, + color=wht, + position=(30, 250), + size=(150, 30), + text="gravity_scale", + click_activate=True, + selectable=True, + on_activate_call=bs.Call(s.welp, -1)) + + bui.textwidget(parent=s.edit_drop_widget, + color=wht, + position=(30, 220), + size=(150, 30), + text="sticky", + click_activate=True, + selectable=True, + on_activate_call=bs.Call(s.welp, -2)) + + bui.textwidget(parent=s.edit_drop_widget, + color=wht, + position=(30, 190), + size=(150, 30), + text="reflection='powerup'", + maxwidth=190, + click_activate=True, + selectable=True, + on_activate_call=bs.Call(s.welp, -3)) + + bui.textwidget(parent=s.edit_drop_widget, + color=wht, + position=(30, 160), + size=(150, 30), + text="reflection='soft'", + maxwidth=190, + click_activate=True, + selectable=True, + on_activate_call=bs.Call(s.welp, -4)) + + bui.textwidget(parent=s.edit_drop_widget, + color=wht, + position=(30, 130), + size=(150, 30), + text="reflection_scale", + maxwidth=190, + click_activate=True, + selectable=True, + on_activate_call=bs.Call(s.welp, -5)) + + s.drop_attr1 = bui.textwidget(parent=s.edit_drop_widget, + color=wht, + position=(220, 250), + editable=True, + size=(70, 30), + text=str(Nice.node_gravity_scale), + description="Default: 1.0, More: Heavier, Less: Lighter, Enter") + + bui.checkboxwidget(parent=s.edit_drop_widget, + value=s.node_sticky, + text="", + color=colb, + scale=s.scale/1.3, + on_value_change_call=bs.Call(s.check_drop_attrs, 0), + position=(225, 220)) + + s.drop_radio1 = bui.checkboxwidget(parent=s.edit_drop_widget, + value=s.node_reflect, + text="", + color=colb, + scale=s.scale/1.3, + on_value_change_call=bs.Call(s.check_drop_attrs, 1), + position=(225, 190)) + + s.drop_radio2 = bui.checkboxwidget(parent=s.edit_drop_widget, + value=s.node_reflect2, + text="", + color=colb, + scale=s.scale/1.3, + on_value_change_call=bs.Call(s.check_drop_attrs, 2), + position=(225, 160)) + + s.drop_attr2 = bui.textwidget(parent=s.edit_drop_widget, + color=wht, + position=(220, 130), + editable=True, + size=(70, 30), + text=str(Nice.node_reflection_scale[0]), + description="Default: 1.2, More: more shiny! while Less: more plain, Enter") + + bacc = bui.buttonwidget( + parent=s.edit_drop_widget, + size=(60, 20), + label='Back', + textcolor=wht, + scale=s.scale, + color=colb, + position=(30, 30), + on_activate_call=bs.Call(s.collect_drop_attrs)) + bui.containerwidget(edit=s.edit_drop_widget, cancel_button=bacc) + + bui.buttonwidget( + parent=s.edit_drop_widget, + size=(60, 20), + label='Help', + color=colb, + textcolor=wht, + scale=s.scale, + position=(200, 30), + on_activate_call=bs.Call(s.welp, 69123)) + + def collect_drop_attrs(s): + t1 = cast(str, bui.textwidget(query=s.drop_attr1)) + t2 = cast(str, bui.textwidget(query=s.drop_attr2)) + try: + v1 = float(t1) + except: + error(f"Invalid gravity_scale value '{t1}'\nrequired value: float\nexample: 6.89") + return + try: + v2 = float(t2) + except: + error(f"Invalid reflection_scale value '{t2}'\nrequired value: float\nexample: 6.89") + return + s.kill(True, s.edit_drop_widget, True) + Nice.node_gravity_scale = v1 + Nice.node_reflection_scale = [float(cast(str, bui.textwidget(query=s.drop_attr2)))] + + def check_drop_attrs(s, i, b): + if not i: + Nice.node_sticky = b + if i == 1: + Nice.node_reflect = b + bui.checkboxwidget(edit=s.drop_radio2, value=False) + Nice.node_reflect2 = False + if i == 2: + Nice.node_reflect2 = b + bui.checkboxwidget(edit=s.drop_radio1, value=False) + Nice.node_reflect = False + + def do_drop(s): + p = Nice.drop_cords + if p[0] == 69123: + error("No position provided") + return + i = Nice.drop_indox + powerup = Nice.drop_indox < 9 + bui.getsound("spawn").play() + with ga().context: + n = None + if powerup: + b = powerup_name[i] + n = PowerupBox(position=p, poweruptype=b).autoretain() + n = n.node + else: + bss = 1.0 + br = 2.0 + if "big_bomb" in Nice.pending2: + bss = 2.5 + br = 12.5 + if i == 9: # TNT + n = Bomb(position=p, bomb_type='tnt', bomb_scale=bss, + blast_radius=br).autoretain() + n = n.node + if i == 10: # Peaceful Mine + n = Bomb(position=p, bomb_type='land_mine', + bomb_scale=bss, blast_radius=br).autoretain() + n = n.node + if i == 11: # Lit Mine + n = Bomb(position=p, bomb_type='land_mine', bomb_scale=bss, + blast_radius=br).autoretain() + n.arm() # returns None + n = n.node + if i > 11 and i < 17: # Eggs + from bascenev1lib.gameutils import SharedObjects + shared = SharedObjects.get() + num = i - 11 + tex = f"eggTex{i - 11}" if i < 15 else "white" if i < 16 else "empty" + n = bs.newnode('prop', + delegate=s, + attrs={ + 'mesh': bs.getmesh("egg"), + 'color_texture': bs.gettexture(tex), + 'body': 'capsule', + 'reflection': 'soft', + 'mesh_scale': 0.5, + 'body_scale': 0.6, + 'density': 4.0, + 'reflection_scale': [0.15], + 'shadow_size': 0.6, + 'position': p, + 'materials': [shared.object_material, bs.Material()], + }, + ) + if i > 16 and i < 21: + n = Bomb(position=p, bomb_type=bomb_type[i - 17]).autoretain() + n = n.node + # apply configs + n.gravity_scale = Nice.node_gravity_scale + n.sticky = Nice.node_sticky + if Nice.node_reflect: + n.reflection = 'powerup' + elif Nice.node_reflect2: + n.reflection = 'soft' + n.reflection_scale = Nice.node_reflection_scale + if "long_fuse" in Nice.pending2: + n.fuse_length = 5 + + def where_to_drop(s): + s.where_drop_widget = s.DW = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 250), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + bui.buttonwidget(parent=s.DW, + size=(200, 50), + label="Current Position", + textcolor=wht, + scale=s.scale, + color=colb, + position=(20, 125), + on_activate_call=bs.Call(s.use_my_pos, 69)) + + bui.textwidget(parent=s.DW, + color=(0.1, 0.7, 1), + text='Where to deploy?', + scale=s.scale, + h_align='center', + v_align='center', + position=(125, 200)) + + bui.buttonwidget(parent=s.DW, + size=(200, 50), + label="Custom Position", + scale=s.scale, + color=colb, + textcolor=wht, + position=(20, 60), + on_activate_call=bs.Call(s.custom_drop_window)) + + bacc = bui.buttonwidget( + parent=s.DW, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, s.DW)) + bui.containerwidget(edit=s.DW, cancel_button=bacc) + + # custom position + def custom_drop_window(s): + s.kill(True, s.DW) + custom_drop_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 250), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + bui.textwidget(parent=custom_drop_widget, + color=(0.1, 0.7, 1), + text='Custom Position', + scale=s.scale, + h_align='center', + v_align='center', + position=(125, 200)) + txt = str(Nice.drop_cords[0]) + x = bui.textwidget( + parent=custom_drop_widget, + text=txt if txt != '69123' else "0", + editable=True, + size=(200, 25), + h_align='center', + v_align='center', + position=(55, 150)) + y = bui.textwidget( + parent=custom_drop_widget, + size=(200, 25), + text=str(Nice.drop_cords[1]), + editable=True, + h_align='center', + v_align='center', + position=(55, 120)) + z = bui.textwidget( + parent=custom_drop_widget, + size=(200, 25), + text=str(Nice.drop_cords[2]), + editable=True, + h_align='center', + v_align='center', + position=(55, 90)) + + def collect(s): + w = x + a = [] + for i in range(3): + try: + a.append(float(cast(str, bui.textwidget(query=w)))) + except: + error("Invalid "+("Z" if w == z else "Y" if w == y else "X")+" Cordinate!") + return + w = z if i else y + s.kill(True, custom_drop_widget) + bui.getsound('gunCocking').play() + Nice.drop_cords = tuple(a) + s.update_cords_view(69) + + def back(s): + s.kill(True, custom_drop_widget) + s.where_to_drop() + + bui.buttonwidget( + parent=custom_drop_widget, + size=(60, 20), + label='Set', + color=colb, + textcolor=wht, + scale=s.scale, + position=(190, 30), + on_activate_call=bs.Call(collect, s)) + + bacc = bui.buttonwidget( + parent=custom_drop_widget, + size=(60, 20), + label='Back', + scale=s.scale, + textcolor=wht, + color=colb, + position=(30, 30), + on_activate_call=bs.Call(back, s)) + bui.containerwidget(edit=custom_drop_widget, cancel_button=bacc) + + def spawn_window(s): + if ga() is None: + push('Spawning requires you to be the host!', color=(1, 1, 0)) + return + global spawn_widget, nice_name, nice_view, cords_view, title_node + spawn_widget = s._sw = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 300), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + bui.textwidget(parent=spawn_widget, + color=(0.1, 0.7, 1), + text='Spawn', + position=(200, 250), + maxwidth=250) + + nice_view = bui.buttonwidget(parent=spawn_widget, + label='', + size=(100, 100), + position=(30, 120), + button_type='square', + color=(1, 1, 1), + texture=bui.gettexture(bot_texture[Nice.indox]+'Icon'), + mask_texture=bui.gettexture('characterIconMask'), + on_activate_call=Picker) + + bui.buttonwidget(edit=nice_view, tint_texture=bui.gettexture( + bot_texture[bot_name.index(Nice.val_attrs[1])]+'IconColorMask')) + bui.buttonwidget( + edit=nice_view, tint_color=Nice.val_attrs[6], tint2_color=Nice.val_attrs[11]) + + cords_view = bui.buttonwidget(parent=spawn_widget, + label='Where To\nSpawn?', + color=colb, + textcolor=wht, + size=(180, 100), + position=(150, 120), + button_type='square', + on_activate_call=bs.Call(s.cords_window)) + + attr_view = bui.buttonwidget(parent=spawn_widget, + label='Edit\nAttrs', + color=colb, + size=(100, 100), + textcolor=wht, + position=(350, 120), + button_type='square', + on_activate_call=bs.Call(s.attr_window)) + + try: + if cords[0] != 69123: + s.update_cords_view() + except TypeError: + error('No coordinates set') + + nice_name = bui.textwidget(parent=spawn_widget, + text=bot_name[Nice.indox], + h_align='center', + v_align='center', + position=(50, 85)) + + bui.buttonwidget(parent=spawn_widget, + label='Locate position', + size=(120, 25), + position=(180, 85), + color=colb, + textcolor=wht, + button_type='square', + on_activate_call=bs.Call(s.show_in_game)) + + bui.buttonwidget(parent=spawn_widget, + label='Draw a line', + size=(120, 25), + position=(180, 50), + button_type='square', + color=colb, + textcolor=wht, + on_activate_call=bs.Call(s.show_in_game, 1)) + + def back(s): + s.kill(True, spawn_widget) + Nice.pending = [] + bacc = bui.buttonwidget( + parent=spawn_widget, + size=(60, 20), + label='Back', + textcolor=wht, + scale=s.scale, + color=colb, + position=(30, 30), + on_activate_call=bs.Call(back, s)) + bui.containerwidget(edit=s._sw, cancel_button=bacc) + + bui.buttonwidget( + parent=spawn_widget, + size=(100, 40), + label='Spawn', + color=colb, + scale=s.scale, + textcolor=wht, + position=(350, 30), + on_activate_call=bs.Call(s.do_spawn)) + + """Button, my little wrappie""" + def Button(s): + def lmao(self): + Nice() + self._resume() + + def openBox(self): + bui.buttonwidget(edit=self.sbox, icon=bui.gettexture('chestOpenIcon')) + bs.apptimer(0.6, bs.Call(closeBox, self)) + + def closeBox(self): + if self.sbox.exists(): + bui.buttonwidget(edit=self.sbox, icon=bui.gettexture('chestIcon')) + + def wrap(self=igm._refresh_in_game, *args, **kwargs): + r = s(self, *args, **kwargs) + h = 125 + v = self._height - 60.0 + self.sbox = bui.buttonwidget( + color=colb, + parent=self._root_widget, + position=(-100, self._height), + size=(100, 50), + scale=1.0, + textcolor=wht, + label="Sandbox", + icon=bui.gettexture('chestIcon'), + iconscale=0.8, + on_select_call=bs.Call(openBox, self), + on_activate_call=bs.Call(lmao, self)) + return r + return wrap + + # coordinates + def cords_window(s): + global cords_widget + cords_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 250), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + bui.buttonwidget(parent=cords_widget, + size=(200, 50), + label="Current Position", + textcolor=wht, + scale=s.scale, + color=colb, + position=(20, 125), + on_activate_call=bs.Call(s.use_my_pos)) + + bui.textwidget(parent=cords_widget, + color=(0.1, 0.7, 1), + text='Where to spawn?', + scale=s.scale, + h_align='center', + v_align='center', + position=(125, 200)) + + bui.buttonwidget(parent=cords_widget, + size=(200, 50), + label="Custom Position", + scale=s.scale, + color=colb, + textcolor=wht, + position=(20, 60), + on_activate_call=bs.Call(s.custom_window)) + + bacc = bui.buttonwidget( + parent=cords_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, cords_widget)) + bui.containerwidget(edit=cords_widget, cancel_button=bacc) + + # custom position + def custom_window(s): + s.kill(True, cords_widget) + global cords + try: + txt = str(cords[0]) if cords[0] != 69123 else "0" + except TypeError: + cords = (0, 0, 0) + txt = "0" + custom_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 250), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + bui.textwidget(parent=custom_widget, + color=(0.1, 0.7, 1), + text='Custom Position', + scale=s.scale, + h_align='center', + v_align='center', + position=(125, 200)) + + x = bui.textwidget( + parent=custom_widget, + text=txt, + editable=True, + size=(200, 25), + h_align='center', + v_align='center', + position=(55, 150)) + y = bui.textwidget( + parent=custom_widget, + size=(200, 25), + text=str(cords[1]), + editable=True, + h_align='center', + v_align='center', + position=(55, 120)) + z = bui.textwidget( + parent=custom_widget, + size=(200, 25), + text=str(cords[2]), + editable=True, + h_align='center', + v_align='center', + position=(55, 90)) + + def collect(s): + global cords + w = x + a = [] + for i in range(3): + try: + a.append(float(cast(str, bui.textwidget(query=w)))) + except: + error("Invalid "+("Z" if w == z else "Y" if w == y else "X")+" Cordinate!") + return + w = z if i else y + s.kill(True, custom_widget) + bui.getsound('gunCocking').play() + cords = tuple(a) + s.update_cords_view() + + def back(s): + s.kill(True, custom_widget) + s.cords_window() + + bui.buttonwidget( + parent=custom_widget, + size=(60, 20), + label='Set', + color=colb, + textcolor=wht, + scale=s.scale, + position=(190, 30), + on_activate_call=bs.Call(collect, s)) + + bacc = bui.buttonwidget( + parent=custom_widget, + size=(60, 20), + label='Back', + scale=s.scale, + textcolor=wht, + color=colb, + position=(30, 30), + on_activate_call=bs.Call(back, s)) + bui.containerwidget(edit=custom_widget, cancel_button=bacc) + + def attr_window(s): + global attr_widget + attr_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(400, 500), + color=cola, + stack_offset=(-s.soff[0], -s.soff[1]), + transition=s.anim_inv, + scale=s.scale) + + attr_scroll = bui.scrollwidget(parent=attr_widget, + position=(30, 80), + claims_up_down=False, + claims_left_right=True, + autoselect=True, + size=(350, 370)) + + bui.textwidget(parent=attr_widget, + color=(0.1, 0.7, 1), + text='Edit Attributes', + scale=s.scale, + h_align='center', + v_align='center', + position=(180, 460)) + + bacc = bui.buttonwidget( + parent=attr_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 45), + on_activate_call=s.gather) + bui.containerwidget(edit=attr_widget, cancel_button=bacc) + + bui.buttonwidget( + parent=attr_widget, + size=(60, 20), + label='Help', + color=colb, + textcolor=wht, + scale=s.scale, + position=(290, 45), + on_activate_call=bs.Call(s.welp, 69123)) + + bui.buttonwidget( + parent=attr_widget, + size=(80, 20), + label='Random', + textcolor=wht, + scale=s.scale, + color=colb, + position=(150, 45), + on_activate_call=bs.Call(s.gather, True)) + + bui.checkboxwidget( + parent=attr_widget, + size=(200, 20), + position=(40, 20), + text="Ran auto spawn", + color=cola, + textcolor=(1, 1, 1), + scale=s.scale/2, + value=s.auto_spawn_on_random, + on_value_change_call=bs.Call(s.tick, 0)) + + bui.checkboxwidget( + parent=attr_widget, + size=(200, 20), + position=(220, 20), + text="Ran peace", + color=cola, + textcolor=(1, 1, 1), + scale=s.scale/2, + value=s.random_peace, + on_value_change_call=bs.Call(s.tick, 1)) + + # -> no = 23 + # -> cw = 595 (+26) + # -> cb = 440 (+20) + # -> tw = 435 (+19) + cw = 757 + cb = 560 + tw = 553 + + et = tw + attr_sub = bui.containerwidget(parent=attr_scroll, + background=False, + size=(190, cw), + color=(0.3, 0.3, 0.3), + scale=s.scale) + global ins + ins = [] + for i in range(len(attrs)): + bui.textwidget(parent=attr_sub, + text=attrs[i], + scale=s.scale/2, + h_align='left', + v_align='center', + on_activate_call=bs.Call(s.welp, i), + selectable=True, + autoselect=True, + click_activate=True, + size=(180, 29), + position=(-30, tw - (20 * i))) + a = Nice.val_attrs[i] + if isinstance(a, bool): + l = bui.checkboxwidget(parent=attr_sub, + value=a, + text="", + color=colb, + scale=s.scale/2, + on_value_change_call=bs.Call(s.check, i), + position=(180, cb - (20 * i))) + elif isinstance(a, tuple) or i == 6 or i == 11 or i == 28: + k = Nice.val_attrs[i] + l = bui.buttonwidget(parent=attr_sub, + label=f"{str(a[0]+0.01)[:3]} {str(a[1]+0.01)[:3]}, {str(a[2]+0.01)[:3]}", + scale=s.scale, + size=(30, 12), + color=k, + textcolor=(1-k[0], 1-k[1], 1-k[2]), # invert + on_activate_call=bs.Call(NicePick, s, a, i), + position=(180, cb - (20 * i))) + else: + l = bui.textwidget(parent=attr_sub, + text=str(a), + scale=s.scale/2, + h_align='left', + v_align='center', + editable=True, + color=(1, 1, 1), + size=(150, 25), + position=(150, et - (20 * i))) + ins.append(l) + + # on back press + def gather(s, ran=False, close=True): + global nice_view + for i in range(len(ins)): + if bui.Widget.get_widget_type(ins[i]) == 'text': + v = cast(str, bui.textwidget(query=ins[i])) + t = type_attrs[i] + if t == 'float': + if ran: + v = random.uniform(0.0, 9.9) + bui.textwidget(edit=ins[i], text=str(v)[:3]) + else: + try: + v = float(v) + except ValueError: + error( + f"{attrs[i]}: Invalid value '{v}'\nRequired type: float, Given type: {type(v).__name__}\nExample of float: 3.141592 (decimal number)") + return + elif t == 'int': + if ran: + v = random.randrange(0, 7) + bui.textwidget(edit=ins[i], text=str(v)) + else: + try: + v = int(v) + except ValueError: + error( + f"{attrs[i]}: Invalid value '{v}'\nRequired type: int, Given type: {type(v)}\nExample of int: 68 (number)") + return + else: + # print (f"checking={v} v_in_bot_name={v in bot_name} not_i={not i} i={i}") + if not v in bot_name and i == 1: + if ran: + v = random.choice(bot_name) + s.spawn(bot_name.index(v)) # update preview + bui.textwidget(edit=ins[i], text=str(v)) + else: + error(f"character: Invalid character '{v}'") + if v in w_bot_name: + push( + f"Did you mean '{bot_name[w_bot_name.index(v)]}'?", color=(0, 0.6, 1)) + return + elif i == 1: + if ran: + v = random.choice(bot_name) + try: + s.spawn(bot_name.index(v)) # update preview + except TypeError: + Nice.spawn(Nice, bot_name.index(v)) + bui.textwidget(edit=ins[i], text=str(v)) + elif not v in bomb_type and i == 8: + if ran: + v = random.choice(bomb_type) + bui.textwidget(edit=ins[i], text=str(v)) + else: + error(f"default_bomb_type: Invalid bomb type '{v}'") + if v in w_bomb_type: + push( + f"Did you mean '{bomb_type[w_bomb_type.index(v)]}'?", color=(0, 0.6, 1)) + return + elif v in bomb_type and ran and i == 8: + v = random.choice(bomb_type) + bui.textwidget(edit=ins[i], text=str(v)) + Nice.val_attrs[i] = v + if bui.Widget.get_widget_type(ins[i]) == 'checkbox' and ran: + v = random.choice([True, False]) if not s.random_peace else False + bui.checkboxwidget(edit=ins[i], value=v) + Nice.val_attrs[i] = v + elif bui.Widget.get_widget_type(ins[i]) == 'button' and ran: + a = [] + for r in range(3): + a.append(random.uniform(0.0, 1.0)) + a = (float(a[0]), float(a[1]), float(a[2])) + bui.buttonwidget(edit=ins[i], label=f"{str(a[0]+0.01)[:3]} {str(a[1]+0.01)[:3]}, {str(a[2]+0.01)[:3]}", color=( + a[0], a[1], a[2]), textcolor=(1-a[0], 1-a[1], 1-a[2])) + Nice.val_attrs[i] = a + bui.buttonwidget(edit=nice_view, tint_texture=bui.gettexture( + bot_texture[bot_name.index(Nice.val_attrs[1])]+'IconColorMask')) + bui.buttonwidget( + edit=nice_view, tint_color=Nice.val_attrs[6], tint2_color=Nice.val_attrs[11]) + if not ran and close: + s.kill(True, attr_widget, rev=True) + elif ran: + bui.getsound('cashRegister2').play() + if s.auto_spawn_on_random: + s.do_spawn() + + def control_window(s): + if ga() is None: + push('How control and you are not the host?', color=(1, 1, 0)) + return + global control_widget, lmao, lmao_bots, old_ga, preview_image, preview_text, dux, preview_text2, preview_text3, start_stop, preview_text4, currently_txt, currently_dux, control_ones, fresh, bomb_control + try: + a = bomb_control + except NameError: + bomb_control = False + fresh = True + + control_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 300), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + try: + p1 = lmao[currently_dux] + p2 = lmao_bots[currently_dux].character + lol = bui.gettexture(bot_texture[bot_name.index(p2)]+"Icon") + except NameError: + p1 = 'Name' + p2 = 'Character' + lol = None + preview_image = bui.buttonwidget(parent=control_widget, + label='', + size=(50, 50), + position=(300, 175), + button_type='square', + color=(1, 1, 1), + texture=lol, + mask_texture=bui.gettexture('characterIconMask'), + on_activate_call=bs.Call(push, 'Set the skin in modify menu')) + + preview_text = bui.textwidget(parent=control_widget, + text=p1, + size=(50, 50), + scale=s.scale/1.3, + position=(365, 175)) + + preview_text2 = bui.textwidget(parent=control_widget, + text=p2, + size=(50, 50), + scale=s.scale/1.7, + position=(360, 155)) + + # '{100 * (1 - lmao_bots[0].node.hurt)}%' + preview_text3 = bui.textwidget(parent=control_widget, + text='', + size=(50, 50), + scale=s.scale/1.7, + position=(295, 125)) + + try: + test = currently_txt + except NameError: + test = 'Control started\nnow tap a bot' + preview_text4 = bui.textwidget(parent=control_widget, + text='Press start\nto start controlling' if not on_control else test, + size=(50, 50), + scale=s.scale/1.7, + position=(295, 85)) + + bui.textwidget(parent=control_widget, + color=(0.1, 0.7, 1), + text='Control', + position=(200, 250), + maxwidth=250) + + start_stop = bui.buttonwidget( + parent=control_widget, + size=(70, 30), + label='Stop' if on_control else 'Start', + icon=bui.gettexture('ouyaAButton' if on_control else 'ouyaOButton'), + iconscale=0.5, + button_type='square', + scale=s.scale, + color=colb, + textcolor=wht, + position=(370, 30), + on_activate_call=bs.Call(s.start_or_stop)) # , True)) + + control_scroll = bui.scrollwidget(parent=control_widget, + position=(30, 80), + claims_up_down=False, + claims_left_right=True, + autoselect=True, + size=(250, 150)) + + control_sub = bui.containerwidget(parent=control_scroll, + background=False, + size=(190, len(lmao)*26), + color=(0.3, 0.3, 0.3), + scale=s.scale) + + bacc = bui.buttonwidget( + parent=control_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, control_widget)) + bui.containerwidget(edit=control_widget, cancel_button=bacc) + + bui.checkboxwidget( + parent=control_widget, + size=(300, 20), + color=cola, + text='Bomb to switch control', + value=bomb_control, + scale=s.scale/2, + position=(120, 35), + textcolor=(1, 1, 1), + on_value_change_call=s.check_bomb) + + if len(lmao) == 0 or str(ga()) != old_ga: + bui.textwidget(parent=control_sub, + text='no bots', + h_align='center', + v_align='center', + size=(60, 29), + position=(60, -62)) + control_ones = [] + for i in range(len(lmao)): + try: + alive = lmao_bots[i].node.hurt < 1 + except IndexError: + s.kill(True, control_widget) + push('Wait for that bot to spawn first') + return + except AttributeError: + alive = False + da_one = bui.textwidget(parent=control_sub, + scale=s.scale/2, + text=(lmao[i] if alive else f"{lmao[i]} (dead)"), + h_align='left', + v_align='center', + color=((1, 1, 1) if alive else (0.6, 0.6, 0.6)), + on_activate_call=bs.Call(s.preview, i, alive), + selectable=True, + autoselect=True, + click_activate=True, + size=(180, 29), + position=(-30, (20 * i))) + control_ones.append(da_one) + try: + control_ones[currently_dux].activate() + except NameError: + pass + + def check_bomb(s, b): + global bomb_control + bomb_control = b + + def preview(s, i, alive, mod=0): + global preview_image, preview_text, lmao, dux, lmao_bots, lmao_chars, preview_text2, preview_text3, drux, val_attrs2, val_arr, on_control, currently_dux, effect_dux + global effect_widget, mod_widget, control_widget, lmao_chars2, lmao2, effect_dux2 + # special case + if i == 69123: + bui.textwidget(edit=preview_text, text='All bots', color=(1, 1, 1)) + bui.textwidget(edit=preview_text2, text='real', color=(0.8, 0.8, 0.8)) + bui.buttonwidget(edit=preview_image, texture=bui.gettexture( + "achievementSharingIsCaring"), tint_texture=None, mask_texture=None) + s.select_all_bots = True + return + s.select_all_bots = False + try: + bui.checkboxwidget(edit=s.select_all, value=False) + except: + pass + drux = i + _lmao_chars = lmao_chars if mod != 3 else lmao_chars2 + _lmao = lmao if mod != 3 else lmao2 + _lmao_bots = lmao_bots if mod != 3 else lmao_players + bui.textwidget(edit=preview_text, text=_lmao[i], color=( + (1, 1, 1) if alive else (0.6, 0.6, 0.6))) + bui.textwidget(edit=preview_text2, text=_lmao_chars[i], color=( + (0.8, 0.8, 0.8) if alive else (0.4, 0.4, 0.4))) + bui.buttonwidget(edit=preview_image, tint_texture=bui.gettexture( + bot_texture[bot_name.index(_lmao_chars[i])]+'IconColorMask')) + if mod != 3: + bui.buttonwidget(edit=preview_image, + tint_color=val_arr[drux][6], tint2_color=val_arr[drux][11]) + if alive: + try: + if not on_control: + s.hl3(i) + except AttributeError: + error('this bot is dead, reopen window') + alive = False + # TODO array containing lmao bot text ins so we can live change + else: + s.hl3(None, False) + try: + hurt = _lmao_bots[i].node.hurt if mod != 3 else _lmao_bots[i].actor.node.hurt + except AttributeError: + pass # bot is GONE + try: + hp_txt = f'HP: {"%.2f" % (100 * (1 - hurt))}%' + except AttributeError: + hp_txt = 'HP: 0.00%' + except NameError: + hp_txt = 'HP: 0.00% (gone)' + bui.textwidget(edit=preview_text3, text=hp_txt, color=( + (0.8, 0.8, 0.8) if alive else (0.4, 0.4, 0.4))) + dux = i + if not mod: + currently_dux = i + elif mod == 2: + effect_dux = i + elif mod == 3: + effect_dux2 = i + bot = _lmao_bots[dux] + char = _lmao_chars[dux] + skin = bot_texture[bot_name.index(char)] # neoSpaz + icon = bui.gettexture(skin+'Icon') # texture: neoSpazIcon + bui.buttonwidget(edit=preview_image, texture=icon, color=( + (1, 1, 1) if alive else (0.6, 0.6, 0.6))) + if mod or on_control: + s.assign() + + def start_or_stop(s): + global on_control, start_stop, lmao, fresh, currently_dux, lmao_bots + try: + KO = lmao_bots[currently_dux].node.hurt == 1 + except NameError: + error("Start your brain first") + return + except AttributeError: + KO = True + if KO: + error(f"{lmao[currently_dux]} is dead.") + return + fresh = False + if not len(lmao): + error('it literally says no bots bruh\nuse spawn menu') + return + on_control = b = not on_control + bui.buttonwidget(edit=start_stop, label='Stop' if b else 'Start') + bui.buttonwidget(edit=start_stop, icon=bui.gettexture( + 'ouyaAButton' if b else 'ouyaOButton')) + if b: + if random.choice([1, 0, 0]): + push('You can switch control by selecting another bot') + s.reset_bright_bots() + KO = False + s.assign() + + def assign(s): # bool): + global on_control, lmao, dux, start_stop, preview_text4, currently_txt, currently_dux, control_widget, preview_text2, old_dux, control_ones, fresh, allow_assign, alive_bots, alive_arr + try: + if control_widget.exists(): + allow_assign = True + if not allow_assign: + return + allow_assign = False # for outside control + except NameError: + return # coming from modify widget lol + for i in ga().players: + if i.sessionplayer.inputdevice.client_id == -1: + i.resetinput() # clean up previous control + with ga().context: + i.actor.connect_controls_to_player() + if not on_control: + push('Stopped control for good', color=(0.4, 0.1, 0.2)) + try: + s.draw() + except: + s.draw(s) + old_dux = None + try: + bui.textwidget(edit=preview_text4, text="Press start\nto start controlling") + except NameError: + pass # modify again + except RuntimeError: + pass # bot died, outside UI control + i.actor.node.invincible = False + return + try: + s.update_alive_bots() + a = lmao_bots[currently_dux].node.hurt + except TypeError: + push('now select a bot to control', color=(0, 0.5, 0)) + return + except AttributeError: + error(f'{lmao[dux]} is dead, controlling nothing') + on_control = False + s.assign() + bui.buttonwidget(edit=start_stop, label='Start', + icon=bui.gettexture("ouyaOButton")) + return + if cast(str, bui.textwidget(query=preview_text4)) == 'Character': + push('good, now select a bot to control', color=(0, 0.5, 0)) + return + if Nice.while_control: + i.actor.node.invincible = True + try: + if currently_dux == old_dux and not fresh: + push('pressed on an already controlled bot') + s.start_or_stop() + return + elif fresh: + fresh = False + except NameError: + pass + old_dux = currently_dux + ding(f'Now controlling {lmao[currently_dux]}') + s.pls_move() + currently_txt = f"Now controlling\n{lmao[currently_dux]}" + s.draw(currently_txt) + bui.textwidget(edit=preview_text4, text=currently_txt) + s.hl2(lmao_bots[currently_dux].node, True) + + # start control from here + i.assigninput(ba.InputType.UP_DOWN, bs.Call(s.set_x)) + i.assigninput(ba.InputType.LEFT_RIGHT, bs.Call(s.set_y)) + i.assigninput(ba.InputType.PICK_UP_PRESS, bs.Call(s.key, 0)) + i.assigninput(ba.InputType.BOMB_PRESS, bs.Call(s.key, 3)) + i.assigninput(ba.InputType.PUNCH_PRESS, bs.Call(s.key, 1)) + i.assigninput(ba.InputType.JUMP_PRESS, bs.Call(s.key, 2)) + break # i have nothing to do w other players left + + def draw(s, what=None, where=(650, 600), color=(0, 1, 1)): + global nood + for i in nood: + i.delete() + if what is None: + return + n = [] + t = what.split('\n') + p = where + c = color + for i in range(len(t)): + with ga().context: + n = bs.newnode("text", attrs={ + "text": t[i], + "flatness": 1.0, + "h_align": "left", + "v_attach": "bottom", + "scale": 0.8, + "position": (p[0], p[1] - (i * 25)), + "color": (c[0]-(i*0.25), c[1]-(i*0.3), c[2]-(i*0.1)) + }) + nood.append(n) + + def config_window(s): + if ga() is None: + push('Sure, ask the HOST that is obv not YOU', color=(1, 1, 0)) + return + global config_widget, epic_config + config_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 350), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=config_widget, + color=(0.1, 0.7, 1), + text='Tune', + position=(205, 303)) + + bui.checkboxwidget(parent=config_widget, + color=cola, + text="Invincible while controlling bots", + textcolor=(1, 1, 1), + value=Nice.while_control, + on_value_change_call=bs.Call(s.conf, 0), + scale=s.scale/1.3, + position=(30, 268)) + + bui.checkboxwidget(parent=config_widget, + color=cola, + text="Notify when my bots die", + textcolor=(1, 1, 1), + value=Nice.notify_bot_ded, + on_value_change_call=bs.Call(s.conf, 1), + scale=s.scale/1.3, + position=(30, 233)) + + bui.checkboxwidget(parent=config_widget, + color=cola, + text="Pause the game when using this", + textcolor=(1, 1, 1), + value=Nice.pause_when_bots, + on_value_change_call=bs.Call(s.conf, 2), + scale=s.scale/1.3, + position=(30, 198)) + + epic_config = bui.checkboxwidget(parent=config_widget, + color=cola, + text="Show screen messages on top right", + textcolor=(1, 1, 1), + value=Nice.top_msg, + on_value_change_call=bs.Call(s.conf, 3), + scale=s.scale/1.3, + position=(30, 163)) +# s.do_your_thing(ga().globalsnode.slow_motion, False) + + bui.checkboxwidget(parent=config_widget, + color=cola, + text="Lite mode (keep off unless lags)", + textcolor=(1, 1, 1), + value=Nice.lite_mode, + on_value_change_call=bs.Call(s.conf, 4), + scale=s.scale/1.3, + position=(30, 128)) + + bui.checkboxwidget(parent=config_widget, + color=cola, + text="Rotate camera on control (cool)", + textcolor=(1, 1, 1), + value=Nice.animate_camera, + on_value_change_call=bs.Call(s.conf, 5), + scale=s.scale/1.3, + position=(30, 93)) + + bui.checkboxwidget(parent=config_widget, + color=cola, + text="Play ding sound on success", + textcolor=(1, 1, 1), + value=Nice.do_ding, + on_value_change_call=bs.Call(s.conf, 6), + scale=s.scale/1.3, + position=(30, 58)) + + bacc = bui.buttonwidget( + parent=config_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, config_widget)) + bui.containerwidget(edit=config_widget, cancel_button=bacc) + + def conf(s, i, b): + if not i: + Nice.while_control = b + var('while_control', b) + elif i == 1: + Nice.notify_bot_ded = b + var('notify_bot_ded', b) + elif i == 2: + Nice.pause_when_bots = b + var('pause_when_bots', b) + s.pause(b) + ding('Applied now!') + elif i == 3: + Nice.top_msg = b + var('top_msg', b) + elif i == 4: + Nice.lite_mode = b + var('lite_mode', b) + elif i == 5: + Nice.animate_camera = b + var('animate_camera', b) + elif i == 6: + Nice.do_ding = b + var('do_ding', b) + + """do your thing, a dumb node extractor that i coded myself + simply extracts titles and changes based on game + eg. Epic Hockey <-> Hockey""" + def do_your_thing(s, b): + import json + global title, virgin, epic_config, title_node + epic = "Epic " if b else "" + + def fade(node, i): + try: + t = title_node[i].text = f"{epic}{title[i]}" + except: + pass + bs.animate(node, 'opacity', {0.0: 0.0, 0.15: 1.0}) + with ga().context: + if virgin: + virgin = False # defined outside as True + title = [] + title_node = [] + # lets grab those nodes! (sus) + for n in bs.getnodes()[::-1]: + if hasattr(n, 'text'): + if 'ARG' in n.text: + continue + if 'gameNames' not in n.text: + continue + try: + try: + title.append(json.loads(n.text)['s'][0][1]['t'][1]) + except: + try: + title.append(json.loads(n.text)['t'][1]) + except: + continue + title_node.append(n) + except: + pass # i swear it cusses about int and stuff i had to shut it up + for i in range(len(title_node)): + if not title_node[i].exists(): + continue + try: + bs.animate(title_node[i], 'opacity', {0.0: 1.0, 0.1: 0.0}) + except: + return # what are we doing here + bs.timer(0.08, bs.Call(fade, title_node[i], i)) + + def mod_window(s): + if ga() is None: + push('Listen, only game host can modify', color=(1, 1, 0)) + return + global mod_widget, lmao, lmao_bots, old_ga, preview_image, preview_text, dux, preview_text2, dux2, preview_text3, do_tp + dux = None + + mod_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 300), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + preview_image = bui.buttonwidget(parent=mod_widget, + label='', + size=(50, 50), + position=(300, 175), + button_type='square', + color=colb, + mask_texture=bui.gettexture('characterIconMask'), + on_activate_call=bs.Call(push, 'Press modify to set the skin and stuff')) + + preview_text = bui.textwidget(parent=mod_widget, + text='', + size=(50, 50), + scale=s.scale/1.3, + position=(365, 175)) + + preview_text2 = bui.textwidget(parent=mod_widget, + text='', + size=(50, 50), + scale=s.scale/1.7, + position=(360, 155)) + + preview_text3 = bui.textwidget(parent=mod_widget, + text='', + size=(50, 50), + scale=s.scale/1.7, + position=(295, 125)) + + bui.textwidget(parent=mod_widget, + color=(0.1, 0.7, 1), + text='Modify', + position=(200, 250), + maxwidth=250) + + bui.buttonwidget( + parent=mod_widget, + size=(70, 30), + label='Modify', + button_type='square', + scale=s.scale, + color=colb, + textcolor=wht, + position=(370, 30), + on_activate_call=bs.Call(s.do_modify)) + + mod_scroll = bui.scrollwidget(parent=mod_widget, + position=(30, 80), + claims_up_down=False, + claims_left_right=True, + autoselect=True, + size=(250, 150)) + + mod_sub = bui.containerwidget(parent=mod_scroll, + background=False, + size=(190, len(lmao)*26), + color=(0.3, 0.3, 0.3), + scale=s.scale) + + bacc = bui.buttonwidget( + parent=mod_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, mod_widget)) + bui.containerwidget(edit=mod_widget, cancel_button=bacc) + + bui.buttonwidget(edit=preview_image, texture=None, color=(1, 1, 1)) + bui.textwidget(edit=preview_text, text='Name') + bui.textwidget(edit=preview_text2, text='Character') + + if len(lmao) == 0 or str(ga()) != old_ga: + bui.textwidget(parent=mod_sub, + text='no bots', + h_align='center', + v_align='center', + size=(60, 29), + position=(60, -62)) + return + + # selected index is dux + for i in range(len(lmao)): + try: + alive = lmao_bots[i].node.hurt < 1 + except IndexError: + s.kill(True, mod_widget) + push('Wait for that bot to spawn first') + return + except AttributeError: + alive = False + bui.textwidget(parent=mod_sub, + scale=s.scale/2, + text=(lmao[i] if alive else f"{lmao[i]} (dead)"), + h_align='left', + v_align='center', + color=((1, 1, 1) if alive else (0.6, 0.6, 0.6)), + on_activate_call=bs.Call(s.preview, i, alive, 1), + selectable=True, + autoselect=True, + click_activate=True, + size=(180, 29), + position=(-30, (20 * i))) + + def tp_check(s, b): + global do_tp + do_tp = b + + def do_modify(s): + global mid_widget, indox2, nice2_name, nice2_view, cords2_view, lmao, dux, lmao_bots, max_digits, val_attrs2, val_arr, drux, cords2, dp_tp + + try: + name = lmao[dux] + except TypeError: + error('You what bro?') + return + try: + a = bot_texture[bot_name.index(val_arr[drux][1])] + except TypeError: + error('It\'s dead.') + return + mid_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 300), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=mid_widget, + color=(0.1, 0.7, 1), + text=f'Modify {name}', + position=(150, 250), + maxwidth=250) + + nice2_view = bui.buttonwidget(parent=mid_widget, + label='', + size=(100, 100), + position=(30, 120), + button_type='square', + color=(1, 1, 1), + texture=bui.gettexture( + bot_texture[bot_name.index(lmao_chars[dux])]+'Icon'), + mask_texture=bui.gettexture('characterIconMask'), + on_activate_call=bs.Call(Picker, 1)) + + # Apply bot's stuff to mod preset (clean up) + cap = bot_name.index(lmao_chars[drux]) + good_name = bot_name[cap] + va = val_arr[drux] + va[1] = good_name + + bui.buttonwidget(edit=nice2_view, tint_texture=bui.gettexture( + bot_texture[bot_name.index(val_arr[drux][1])]+'IconColorMask')) + bui.buttonwidget( + edit=nice2_view, tint_color=val_arr[drux][6], tint2_color=val_arr[drux][11]) + + try: + pus = lmao_bots[dux].node.position + except AttributeError: + error(f'{lmao[dux]} is dead.') + return + m = max_digits + cords2_view = bui.buttonwidget(parent=mid_widget, + label=f'changed via\nupdate_cords_view', + color=colb, + textcolor=wht, + size=(180, 100), + position=(150, 120), + button_type='square', + on_activate_call=bs.Call(s.cords2_window)) + + attr_view = bui.buttonwidget(parent=mid_widget, + label='Edit\nAttrs', + color=colb, + size=(100, 100), + textcolor=wht, + position=(350, 120), + button_type='square', + on_activate_call=bs.Call(s.do_modify2)) + + s.update_cords_view(True) + + nice2_name = bui.textwidget(parent=mid_widget, + text=good_name, + h_align='center', + v_align='center', + position=(50, 85)) + + bui.buttonwidget(parent=mid_widget, + label='Locate position', + size=(120, 25), + position=(180, 85), + color=colb, + textcolor=wht, + button_type='square', + on_activate_call=bs.Call(s.show_in_game, 0, True)) + + bui.buttonwidget(parent=mid_widget, + label='Draw a line', + size=(120, 25), + position=(180, 50), + button_type='square', + color=colb, + textcolor=wht, + on_activate_call=bs.Call(s.show_in_game, 1, True)) + + bacc = bui.buttonwidget( + parent=mid_widget, + size=(60, 20), + label='Back', + textcolor=wht, + scale=s.scale, + color=colb, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, mid_widget, True)) + bui.containerwidget(edit=mid_widget, cancel_button=bacc) + + bui. buttonwidget( + parent=mid_widget, + size=(100, 40), + label='Apply', + color=colb, + scale=s.scale, + textcolor=wht, + position=(350, 30), + on_activate_call=s.apply_mods) + + bui.checkboxwidget( + parent=mid_widget, + size=(70, 30), + text="Teleport", + value=do_tp, + color=cola, + textcolor=(1, 1, 1), + on_value_change_call=bs.Call(s.tp_check), + scale=s.scale/1.5, + position=(340, 90)) + + val_attrs2 = val_arr[dux].copy() # reset to default temp + indox2 = bot_name.index(val_attrs2[1]) + + def do_modify2(s): + global dux, lmao_bots, mud_widget, val_attrs2 + try: + if lmao_bots[dux].node.hurt >= 1: + error(f'{lmao[dux]} is dead.') + return + except TypeError: + error('You what bro?') + return + mud_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(400, 500), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + + mud_scroll = bui.scrollwidget(parent=mud_widget, + position=(30, 80), + claims_up_down=False, + claims_left_right=True, + autoselect=True, + size=(350, 370)) + + bui.textwidget(parent=mud_widget, + color=(0.1, 0.7, 1), + text='Edit attributes', + scale=s.scale, + h_align='center', + v_align='center', + position=(180, 460)) + + bacc = bui.buttonwidget( + parent=mud_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=s.gather2) + bui.containerwidget(edit=mud_widget, cancel_button=bacc) + + bui.buttonwidget( + parent=mud_widget, + size=(60, 20), + label='Help', + color=colb, + textcolor=wht, + scale=s.scale, + position=(290, 30), + on_activate_call=bs.Call(s.welp, 69123)) + + bui.buttonwidget( + parent=mud_widget, + size=(80, 20), + label='Random', + textcolor=wht, + scale=s.scale, + color=colb, + position=(150, 30), + on_activate_call=bs.Call(s.gather2, True)) + + # -> no = 23 + # -> cw = 595 (+26) + # -> cb = 440 (+20) + # -> tw = 435 (+19) + cw = 757 + cb = 560 + tw = 553 + + et = tw + mud_sub = bui.containerwidget(parent=mud_scroll, + background=False, + size=(190, cw), + color=(0.3, 0.3, 0.3), + scale=s.scale) + global ins2 + ins2 = [] + for i in range(len(attrs)): + bui.textwidget(parent=mud_sub, + text=attrs[i], + scale=s.scale/2, + h_align='left', + v_align='center', + on_activate_call=bs.Call( + s.welp, i) if i not in not_editable else bs.Call(s.welp, i, nah=True), + selectable=True, + autoselect=True, + color=(1, 1, 1) if i not in not_editable else (0.6, 0.6, 0.6), + click_activate=True, + size=(180, 29), + position=(-30, tw - (20 * i))) + a = val_attrs2[i] + if isinstance(a, bool): + l = bui.checkboxwidget(parent=mud_sub, + value=a, + text="", + color=colb, + scale=s.scale/2, + on_value_change_call=bs.Call( + s.check, i, mod=True) if i not in not_editable else bs.Call(s.welp, i, ignore=True), + position=(180, cb - (20 * i))) + elif isinstance(a, tuple) or i == 6 or i == 11 or i == 28: + k = val_attrs2[i] + l = bui.buttonwidget(parent=mud_sub, + label=f"{str(a[0]+0.01)[:3]} {str(a[1]+0.01)[:3]}, {str(a[2]+0.01)[:3]}", + scale=s.scale, + size=(30, 12), + color=k, + textcolor=(1-k[0], 1-k[1], 1-k[2]), # invert + on_activate_call=bs.Call(NicePick2, s, a, i), + position=(180, cb - (20 * i))) + else: + l = bui.textwidget(parent=mud_sub, + text=str(a), + scale=s.scale/2, + h_align='left', + v_align='center', + editable=True, + color=(1, 1, 1), + size=(150, 25), + position=(150, et - (20 * i))) + ins2.append(l) + + def gather2(s, ran=False): + global val_attrs2, nice2_view, mud_widget, drux + for i in range(len(ins2)): + if bui.Widget.get_widget_type(ins2[i]) == 'text': + v = cast(str, bui.textwidget(query=ins2[i])) + t = type_attrs[i] + if t == 'float': + if ran: + v = random.uniform(0.0, 9.9) + bui.textwidget(edit=ins2[i], text=str(v)[:3]) + else: + try: + v = float(v) + except ValueError: + error( + f"{attrs[i]}: Invalid value '{v}'\nRequired type: float, Given type: {type(v).__name__}\nExample of float: 3.141592 (decimal number)") + return + elif t == 'int': + if ran: + v = random.randrange(0, 7) + bui.textwidget(edit=ins2[i], text=str(v)) + else: + try: + v = int(v) + except ValueError: + error( + f"{attrs[i]}: Invalid value '{v}'\nRequired type: int, Given type: {type(v)}\nExample of int: 68 (number)") + return + else: + # print (f"checking={v} v_in_bot_name={v in bot_name} not_i={not i} i={i}") + if not v in bot_name and i == 1: + if ran: + v = random.choice(bot_name) + s.spawn(bot_name.index(v)) # update preview + bui.textwidget(edit=ins2[i], text=str(v)) + else: + error(f"character: Invalid character '{v}'") + if v in w_bot_name: + push( + f"Did you mean '{bot_name[w_bot_name.index(v)]}'?", color=(0, 0.6, 1)) + return + elif i == 1: + if ran: + v = random.choice(bot_name) + s.spawn(bot_name.index(v), True) # update preview + bui.textwidget(edit=ins2[i], text=str(v)) + elif not v in bomb_type and i == 8: + if ran: + v = random.choice(bomb_type) + bui.textwidget(edit=ins2[i], text=str(v)) + else: + error(f"default_bomb_type: Invalid bomb type '{v}'") + if v in w_bomb_type: + push( + f"Did you mean '{bomb_type[w_bomb_type.index(v)]}'?", color=(0, 0.6, 1)) + return + elif v in bomb_type and ran and i == 8: + v = random.choice(bomb_type) + bui.textwidget(edit=ins2[i], text=str(v)) + val_attrs2[i] = v + if bui.Widget.get_widget_type(ins2[i]) == 'checkbox' and ran: + v = random.choice([True, False]) + bui.checkboxwidget(edit=ins2[i], value=v) + val_attrs2[i] = v + elif bui.Widget.get_widget_type(ins2[i]) == 'button' and ran: + a = [] + for r in range(3): + a.append(random.uniform(0.0, 1.0)) + a = (float(a[0]), float(a[1]), float(a[2])) + bui.buttonwidget(edit=ins2[i], label=f"{str(a[0]+0.01)[:3]} {str(a[1]+0.01)[:3]}, {str(a[2]+0.01)[:3]}", color=( + a[0], a[1], a[2]), textcolor=(1-a[0], 1-a[1], 1-a[2])) + val_attrs2[i] = a +# bui.buttonwidget(edit=nice2_view, tint_texture=bui.gettexture(val_attrs2[1]+'IconColorMask')) + bui.buttonwidget(edit=nice2_view, tint_texture=bui.gettexture( + bot_texture[bot_name.index(val_attrs2[1])]+'IconColorMask')) + bui.buttonwidget( + edit=nice2_view, tint_color=val2_attrs[6], tint2_color=val2_attrs[11]) + if not ran: + s.kill(True, mud_widget, True) + else: + bui.getsound('cashRegister2').play() + + def cords2_window(s): + global cords2_widget + cords2_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 250), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + bui.buttonwidget(parent=cords2_widget, + size=(200, 50), + label="Current Position", + textcolor=wht, + scale=s.scale, + color=colb, + position=(20, 125), + on_activate_call=bs.Call(s.use_my_pos, True)) + + bui.textwidget(parent=cords2_widget, + color=(0.1, 0.7, 1), + text='Teleport to:', + scale=s.scale, + h_align='center', + v_align='center', + position=(125, 200)) + + bui.buttonwidget(parent=cords2_widget, + size=(200, 50), + label="Custom Position", + scale=s.scale, + color=colb, + textcolor=wht, + position=(20, 60), + on_activate_call=bs.Call(s.custom2_window)) + + bacc = bui.buttonwidget( + parent=cords2_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, cords2_widget, True)) + bui.containerwidget(edit=cords2_widget, cancel_button=bacc) + + def custom2_window(s): + s.kill(True, cords2_widget, True) + global cords2 + try: + txt = str(cords2[0]) if cords2[0] != 69123 else "0" + except TypeError: + cords2 = (0, 0, 0) + txt = "0" + custom2_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(300, 250), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + bui.textwidget(parent=custom2_widget, + color=(0.1, 0.7, 1), + text='Custom Position', + scale=s.scale, + h_align='center', + v_align='center', + position=(125, 200)) + + x = bui.textwidget( + parent=custom2_widget, + text=txt, + editable=True, + size=(200, 25), + h_align='center', + v_align='center', + position=(55, 150)) + y = bui.textwidget( + parent=custom2_widget, + size=(200, 25), + text=str(cords2[1]), + editable=True, + h_align='center', + v_align='center', + position=(55, 120)) + z = bui.textwidget( + parent=custom2_widget, + size=(200, 25), + text=str(cords2[2]), + editable=True, + h_align='center', + v_align='center', + position=(55, 90)) + + def collect(s): + global cords2 + w = x + a = [] + for i in range(3): + try: + a.append(float(cast(str, bui.textwidget(query=w)))) + except: + error("Invalid "+("Z" if w == z else "Y" if w == y else "X")+" Cordinate!") + return + w = z if i else y + s.kill(True, custom2_widget, True) + bui.getsound('gunCocking').play() + cords2 = tuple(a) + s.update_cords_view(True) + + def back(s): + s.kill(True, custom2_widget, True) + s.cords2_window(Nice) + + bui.buttonwidget( + parent=custom2_widget, + size=(60, 20), + label='Set', + color=colb, + textcolor=wht, + scale=s.scale, + position=(190, 30), + on_activate_call=bs.Call(collect, s)) + + bacc = bui.buttonwidget( + parent=custom2_widget, + size=(60, 20), + label='Back', + scale=s.scale, + textcolor=wht, + color=colb, + position=(30, 30), + on_activate_call=bs.Call(back, s)) + bui.containerwidget(edit=custom2_widget, cancel_button=bacc) + + def apply_mods(s): + global drux, lmao, lmao_bots, val_arr, indox2, val_attts2, cords2, do_tp + global LAH, LAP, LAB, LAF, testa + s.kill(True, mid_widget, True) + new = val_attrs2 + bot = lmao_bots[dux] + if cords2[0] != 69123 and do_tp: + bot.node.handlemessage(bs.StandMessage(cords2, 0)) + nice_custom_color = (0.7, 0.7, 0.7) + bot.bouncy = new[0] + # skipped character + bot.charge_dist_max = new[2] + bot.charge_dist_min = new[3] + charge_speed_max = new[4] + charge_speed_min = new[5] + bot.node.color = new[6] + bot.set_bomb_count(new[7]) + bot.bomb_type = new[8] + bot.bomb_type_default = new[8] + bot.default_boxing_gloves = new[9] + bot.default_shields = new[10] + bot.node.highlight = new[11] + bot.punchiness = new[12] + bot.run = new[13] + bot.run_dist_min = new[14] + bot.demo_mode = new[15] + bot.static = new[16] + bot.throw_dist_max = new[17] + bot.throw_dist_min = new[18] + bot.throw_rate = new[19] + bot.throwiness = new[20] + bot.start_invincible = new[22] + LAH[dux] = new[23] + LAP[dux] = new[24] + LAB[dux] = new[25] + LAF[dux] = new[26] + if new[27] == '%': + testa[dux].text = new[1] + elif new[27] == '$': + testa[dux].text = lmao[drux] + else: + testa[dux].text = new[27] + testa[dux].color = new[28] + t = bot_texture[bot_name.index(bot.character)] + s.set_char(bot, bot_style[indox2]) + ding(f'Modified {lmao[drux]}!') + if not on_control: + s.hl4(bot) + val_arr[dux] = val_attrs2.copy() # apply temp to the stored ones + + def set_char(s, bot, char): + global lmao_bots, lmao_chars, val_arr + i = lmao_bots.index(bot) + name = bot_name[bot_style.index(char)] + lmao_chars[i] = name + val_arr[i][1] = name + b = bot.node + c = bot_texture[bot_style.index(char)] + with ga().context: + try: + pelvis = bs.getmesh(c+'Pelvis') + except RuntimeError: + pelvis = bs.getmesh('kronkPelvis') + head = bs.getmesh(c+'Head') + torso = bs.getmesh(c+'Torso') + toes = bs.getmesh(c+'Toes') + uarm = bs.getmesh(c+'UpperArm') + uleg = bs.getmesh(c+'UpperLeg') + farm = bs.getmesh(c+'ForeArm') + lleg = bs.getmesh(c+'LowerLeg') + hand = bs.getmesh(c+'Hand') + b.head_mesh = head + b.pelvis_mesh = pelvis + b.torso_mesh = torso + b.toes_mesh = toes + b.upper_arm_mesh = uarm + b.upper_leg_mesh = uleg + b.forearm_mesh = farm + b.lower_leg_mesh = lleg + b.hand_mesh = hand + b.style = 'spaz' if char in has_no_style else char + b.color_mask_texture = bs.gettexture(c+'ColorMask') + b.color_texture = bs.gettexture(c if c in has_no_color else c+'Color') + s.preview(i, (bot.node.hurt < 1), 1) + + def listen_window(s): + global listen_widget, music_preview_image, wmusic_preview_text, music_preview_text2, music_dux + music_dux = 8 + listen_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 300), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + music_preview_image = bui.buttonwidget(parent=listen_widget, + label='', + size=(50, 50), + position=(300, 175), + button_type='square', + color=colb, + mask_texture=bui.gettexture('characterIconMask')) + + music_preview_text = bui.textwidget(parent=listen_widget, + text='', + size=(50, 50), + scale=s.scale/1.4, + maxwidth=115, + position=(365, 175)) + + music_preview_text2 = bui.textwidget(parent=listen_widget, + text='', + size=(50, 50), + scale=s.scale/1.7, + maxwidth=115, + position=(360, 155)) + + bui.textwidget(parent=listen_widget, + color=(0.1, 0.7, 1), + text='Listen', + position=(200, 250), + maxwidth=150) + + bui.buttonwidget( + parent=listen_widget, + size=(70, 30), + label='Listen', + button_type='square', + scale=s.scale, + color=colb, + textcolor=wht, + position=(370, 30), + on_activate_call=bs.Call(s.play_music)) + + bui.buttonwidget( + parent=listen_widget, + size=(70, 30), + label='Def', + button_type='square', + scale=s.scale, + icon=bui.gettexture("replayIcon"), + iconscale=s.scale/2.5, + color=colb, + textcolor=wht, + position=(270, 30), + on_activate_call=bs.Call(s.play_music, True)) + + listen_scroll = bui.scrollwidget(parent=listen_widget, + position=(30, 80), + claims_up_down=False, + claims_left_right=True, + autoselect=True, + size=(250, 150)) + + listen_sub = bui.containerwidget(parent=listen_scroll, + background=False, + size=(190, len(music_name)*26), + color=(0.3, 0.3, 0.3), + scale=s.scale) + + bacc = bui.buttonwidget( + parent=listen_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, listen_widget)) + bui.containerwidget(edit=listen_widget, cancel_button=bacc) + + bui.buttonwidget(edit=music_preview_image, texture=None, color=(1, 1, 1)) + bui.textwidget(edit=music_preview_text, text=music_name[music_dux]) + bui.textwidget(edit=music_preview_text2, text=music_desc[music_dux]) + bui.buttonwidget(edit=music_preview_image, texture=bui.gettexture(music_texture[music_dux])) + + # selected index is music_dux + for i in range(len(music_name)): + bui.textwidget(parent=listen_sub, + scale=s.scale/2, + text=(music_name[i]), + h_align='left', + v_align='center', + color=(1, 1, 1), + on_activate_call=bs.Call(s.preview_music, i), + selectable=True, + autoselect=True, + click_activate=True, + size=(180, 29), + position=(-30, (20 * i))) + + def preview_music(s, i): + global music_preview_image, music_dux, music_preview_text, music_preview_text2 + global music_widget + music_dux = i + bui.textwidget(edit=music_preview_text, text=music_name[i], color=(1, 1, 1)) + bui.textwidget(edit=music_preview_text2, text=music_desc[i], color=(1, 1, 1)) + bui.buttonwidget(edit=music_preview_image, texture=bui.gettexture(music_texture[i])) + + def play_music(s, default=False): + global music_dux + try: + with ga().context: + bs.setmusic(music_type[music_dux] if not default else ga().default_music) + except AttributeError: + if not default: + bs.set_internal_music(ba.getsimplesound(music_desc[music_dux][:-4])) + push("You are not the host,\nsound will only play for you\nand it might be lower than usual\nturn sound volume down and music volume up", color=(1, 0, 1)) + if default: + try: + push( + f"Now playing default music: {music_name[music_type.index(ga().default_music)]}") + except AttributeError: + push("Unable to get default music\nsince you are not the host\nit resets next game tho", (1, 1, 0)) + + def effect_window(s): + if ga() is None: + push('Effect who and how? you are not the host!', color=(1, 1, 0)) + return + global effect_widget, lmao, lmao_bots, old_ga, preview_image, preview_text, dux, preview_text2, dux2, preview_text3, effect_dux, effect_ones, effect_tab, effect_sub, effect_bots + effect_bots = True + effect_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 290), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + try: + p1 = lmao[effect_dux] + p2 = lmao_bots[effect_dux].character + lol = bui.gettexture(bot_texture[bot_name.index(p2)]+"Icon") + except IndexError: + p1 = 'Name' + p2 = 'Character' + lol = None + preview_image = bui.buttonwidget(parent=effect_widget, + label='', + size=(50, 50), + position=(300, 175), + button_type='square', + color=(1, 1, 1), + texture=lol, + mask_texture=bui.gettexture('characterIconMask'), + on_activate_call=bs.Call(push, 'what are you trying to achieve')) + + preview_text = bui.textwidget(parent=effect_widget, + text=p1, + size=(50, 50), + scale=s.scale/1.3, + position=(365, 175)) + + preview_text2 = bui.textwidget(parent=effect_widget, + text=p2, + size=(50, 50), + scale=s.scale/1.7, + position=(360, 155)) + + # '{100 * (1 - lmao_bots[0].node.hurt)}%' + preview_text3 = bui.textwidget(parent=effect_widget, + text='', + size=(50, 50), + scale=s.scale/1.7, + position=(295, 125)) + + bui.textwidget(parent=effect_widget, + text='Select who,\nthen press effect', + size=(50, 50), + scale=s.scale/1.7, + position=(295, 85)) + + bui.textwidget(parent=effect_widget, + color=(0.1, 0.7, 1), + text='Effect', + position=(300, 240), + maxwidth=250) + + bui.buttonwidget( + parent=effect_widget, + size=(70, 30), + label='Effect', + button_type='square', + scale=s.scale, + color=colb, + textcolor=wht, + position=(360, 30), + on_activate_call=bs.Call(s.do_effect)) + + effect_scroll = bui.scrollwidget(parent=effect_widget, + position=(30, 80), + claims_up_down=False, + claims_left_right=True, + autoselect=True, + size=(250, 150)) + tabdefs = [('bots', 'Bots'), ('players', "Players")] + + effect_tab = TabRow( + effect_widget, + tabdefs, + pos=(30, 230), + size=(250, 0), + on_select_call=s.switch_tab) + + effect_tab.update_appearance('bots') + effect_sub = bui.containerwidget(parent=effect_scroll, + background=False, + color=(0.3, 0.3, 0.3), + scale=s.scale) + bacc = bui.buttonwidget( + parent=effect_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, effect_widget)) + bui.containerwidget(edit=effect_widget, cancel_button=bacc) + + s.cola_fill(effect_widget) + + if len(lmao) == 0 or str(ga()) != old_ga: + s.inform('No bots', effect_sub) + else: + try: + a = effect_dux + except NameError: + a = 0 + effect_ones = s.load(lmao, lmao_bots, effect_widget, effect_sub, a) + + def switch_tab(s, id): + global effect_tab, effect_sub, effect_widget, preview_image, effect_dux, lmao_players, lmao, lmao_bots, lmao2, lmao_chars2, effect_bots + effect_tab.update_appearance(id) + for w in effect_sub.get_children(): + w.delete() + if id == "bots": + effect_bots = True + try: + a = effect_dux + except NameError: + a = 0 + s.load(lmao, lmao_bots, effect_widget, effect_sub, a) + else: + effect_bots = False + s.select_all_bots = False + lmao_players = ga().players + lmao2 = [] + lmao_chars2 = [] + for i in lmao_players: + lmao2.append(i.getname()) + lmao_chars2.append(i.character) + s.load(lmao2, lmao_players, effect_widget, effect_sub, 0, 3) + s.cola_fill(effect_widget) + + def load(s, arr, arr2, container, sub, dux=0, mod=2): + global lmao_bots, lmao_players, preview_image + bui.containerwidget(edit=sub, size=(190, ((1 if mod == 2 else 0)+len(arr))*26)) + if len(arr) == 0 or str(ga()) != old_ga and mod == 2: + s.inform('Still\nNo bots', effect_sub) + return + ones = [] + for i in range(len(arr)): + try: + alive = (arr2[i].node.hurt < + 1) if arr2[i] in lmao_bots else arr2[i].actor.node.hurt < 1 + except IndexError: + s.kill(True, widget) + push('Something is still spawining, try again') + return + except AttributeError: + alive = False + da_one = bui.textwidget(parent=sub, + scale=s.scale/2, + text=(arr[i] if alive else f"{arr[i]} (dead)"), + h_align='left', + v_align='center', + color=((1, 1, 1) if alive else (0.6, 0.6, 0.6)), + on_activate_call=bs.Call(s.preview, i, alive, mod), + selectable=True, + autoselect=True, + click_activate=True, + size=(180, 29), + position=(-30, (20 * i))) + ones.append(da_one) + if mod == 2: + s.select_all = bui.checkboxwidget(parent=sub, + scale=s.scale/2, + size=(200, 5), + text="Select all", + color=cola, + value=False, + textcolor=wht, + on_value_change_call=s.effect_all_bots, + position=(0, 10+(20 * (len(arr))))) + try: + ones[dux].activate() + except NameError: + pass + return ones + + def effect_all_bots(s, b): + if b: + s.preview(69123, True) + s.reset_bright_bots() + s.select_all_bots = b + + def inform(s, what, where): + global nukeme + try: + nukeme.delete() + except NameError: + pass + for i in where.get_children(): + i.delete() + nukeme = bui.textwidget(parent=where, + text=what, + h_align='center', + v_align='center', + size=(60, 29), + position=(60, -62)) + + def do_effect(s): + global effect_dux, lmao_bots, lmao, effect_bots, lmao2, lmao_players, eff_widget, indox2, nice2_name, nice2_view, cords2_view, dux, max_digits, val_attrs2, val_arr, drux, cords2, dp_tp, effect_indox, effect_tip, effect_dux2 + # validate button press + if not s.select_all_bots: + _lmao = lmao if effect_bots else lmao2 + _lmao_bots = lmao_bots if effect_bots else lmao_players + _effect_dux = effect_dux if effect_bots else effect_dux2 + try: + name = _lmao[_effect_dux] + except NameError: + error('Select a bot first' if len(_lmao_bots) + else 'When it says no bots\nyet u still click the button??') + return + except IndexError: + pass + try: + hurt = _lmao_bots[_effect_dux].node.hurt if effect_bots else _lmao_bots[_effect_dux].actor.node.hurt + if hurt == 1: + error(f'{_lmao[_effect_dux]} is dead.') + return + except: + try: + error(f'{_lmao[_effect_dux]} is dead.') + return + except IndexError: + error("No bots") + return + else: + _lmao = lmao + _lmao_bots = lmao_bots + _effect_dux = 69123 + name = "All bots" + try: + a = effect_indox + except NameError: + effect_indox = 0 + eff_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(500, 300), + stack_offset=s.soff, + color=cola, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=eff_widget, + color=(0.1, 0.7, 1), + text=f'Effect {name}', + position=(165, 250), + maxwidth=250) + + effect_tip = bui.textwidget(parent=eff_widget, + text=effect_tips[effect_indox], + position=(175, 190), + scale=s.scale/2, + maxwidth=250) + + nice2_view = bui.buttonwidget(parent=eff_widget, + label='', + size=(100, 100), + position=(60, 120), + button_type='square', + color=(1, 1, 1), + texture=bui.gettexture(effect_texture[effect_indox]), + mask_texture=bui.gettexture('characterIconMask'), + on_activate_call=bs.Call(Picker, 2)) + + # Apply bot's stuff to mod preset (clean up) + if not s.select_all_bots: + good_name = effect_name[_effect_dux] + if effect_bots: + va = val_arr[_effect_dux] + bui.buttonwidget(edit=nice2_view, tint_texture=bui.gettexture( + effect_texture[_effect_dux])) + + nice2_name = bui.textwidget(parent=eff_widget, + text=effect_name[effect_indox], + h_align='center', + v_align='center', + position=(85, 85)) + + bacc = bui.buttonwidget( + parent=eff_widget, + size=(60, 20), + label='Back', + textcolor=wht, + scale=s.scale, + color=colb, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, eff_widget, True)) + bui.containerwidget(edit=eff_widget, cancel_button=bacc) + + bui. buttonwidget( + parent=eff_widget, + size=(100, 40), + label='Apply', + color=colb, + scale=s.scale, + textcolor=wht, + position=(350, 30), + on_activate_call=s.apply_effects) + + if s.select_all_bots: + return + if effect_bots: + val_attrs2 = val_arr[_effect_dux].copy() # reset to default temp + indox2 = effect_name.index(good_name) + + def apply_effects(s): + global eff_widget, effect_indox, effect_dux, lmao_bots, lmao, effect_bots, effect_dux + s.kill(True, eff_widget, True) + n = effect_indox + i = effect_dux if effect_bots else effect_dux2 + a = effect_name[n] + if not s.select_all_bots: + bots = [lmao_bots[i]] if effect_bots else [lmao_players[i].actor] + name = lmao[i] if effect_bots else lmao2[i] + else: + bots = lmao_bots + name = "All Bots" + try: + em = effect_message[n] + except IndexError: + em = None + with ga().context: + for bot in bots: + if em: + bot.handlemessage(bs.PowerupMessage(em)) + elif a == 'Shatter': + bot.shatter(True) + elif a == 'Freeze': + bot.handlemessage(bs.FreezeMessage()) + elif a == 'Unfreeze': + bot.handlemessage(bs.ThawMessage()) + elif a == 'Celebrate': + bot.handlemessage(bs.CelebrateMessage()) + elif a == 'Stop Celebrating': + bot.handlemessage(bs.CelebrateMessage(duration=0.0001)) + elif a == 'Kill': + try: + bot.handlemessage(bs.DieMessage()) + except: + pass # bot is dead or so + elif a == 'Infinite Curse': + if bot._cursed: + bot.handlemessage(bs.PowerupMessage('health')) + bot.curse() + s.spam_curse(bot) + elif a == 'Super Speed': + bot.node.hockey = True + elif a == 'Normal Speed': + bot.node.hockey = False + elif a == 'Invincible': + bot.node.invincible = True + elif a == 'Beatable': + bot.node.invincible = False + elif a == 'Sleep': + bot._knocked = True + s.spam_knock(bot) + elif a == 'Wake Up': + bot._knocked = False + elif a == 'Super Punch': + s.give_sp(bot) + elif a == 'Normal Punch': + bot._punch_power_scale = 1.2 + bot._punch_cooldown = 400 + elif a == 'Fly Jumps': + if effect_bots: + bot.on_jump_press = s.spaz_bot_fly(bot.on_jump_press) + else: + lmao_players[i].assigninput( + ba.InputType.JUMP_PRESS, bs.Call(s.spaz_fly, bot)) + elif a == 'Normal Jumps': + bot.on_jump_press = s.spaz_not_fly + elif a == 'GodMode Preset': + bot.node.hockey = True # Speed + bot._super = True + bot.node.invincible = True # Invincibility + s.give_sp(bot) # Super Punch + PopupText("I HAVE THE POWER", position=bot.node.position, + random_offset=1).autoretain() + elif a == "Reset All": + push(f'Resetted all effects from {name}') + bui.getsound('shieldDown').play() + bot.on_jump_press = s.spaz_not_fly + bot._cursed = False + bot._super = False + bot.node.hockey = False + bot.node.invincible = False + bot._knocked = False + bot._punch_power_scale = 1.2 + bot._punch_cooldown = 400 + return + ding(f"Applied '{a}' to {name}") + + def link_text(s, text, bot, color=(1, 1, 1), off=1.5): + with ga().context: + try: + m = bs.newnode('math', + owner=bot.node, + attrs={'input1': (0, off, 0), + 'operation': 'add'}) + bot.node.connectattr('position', m, 'input2') + test = bs.newnode( + 'text', + owner=bot.node, + attrs={'text': text, + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': color, + 'scale': 0.0, + 'h_align': 'center'}) + m.connectattr('output', test, 'position') + bs.animate(test, 'scale', {0: 0.0, 0.5: 0.01}) + return test + except: + pass + + def nodetimer(s, time, node): + with ga().context: + bs.timer(time, node.delete) + + """Constant Jump - spam jump on bot, combine w fly jumps""" + def constant_jump(s, bot): + if not bot.exists(): + return + p = bot.node.position + p2 = (p[0], p[1]-0.2, p[2]) + bot.on_jump_press(bot) + if random.choice([False, False, False, True]): + PopupText("Hoppie", position=p2, random_offset=0.3, color=(1, 0, 1)).autoretain() + bs.timer((random.choice([0.1, 0.4, 0.7, 0.1]) if p2[1] < + 4 else 1.5), bs.Call(s.constant_jump, s, bot)) + + """toxic celebrate - when a player dies, + celebrate the hunt, called from outside Nice.""" + def toxic_celebrate(s): + for b in Nice.toxic_bots: + try: + p = b.node.position + except: + return # bot is dead + p2 = (p[0], p[1]-0.2, p[2]) +# PopupText(random.choice(toxic_win),position=p2,random_offset=0.3,color=(0,1,1)).autoretain() + n = s.link_text(s, text=random.choice(toxic_win), bot=b, color=(1, 0, 1), off=2) + s.nodetimer(s, 1.5, n) + + """Constant Heal - heal bot from time to time""" + def constant_heal(s, bot): + with ga().context: + if not bot.exists(): + return + p = bot.node.position + p2 = (p[0], p[1]-0.2, p[2]) + bot.handlemessage(bs.PowerupMessage('health')) + PopupText("Healed", position=p2, random_offset=0.3, color=(0, 1, 0)).autoretain() + bs.timer(4, bs.Call(s.constant_heal, s, bot)) + + """Make Toxic - makes a bot say toxic stuff. + only called from outside Nice""" + def make_toxic(s, bot): + with ga().context: + if not bot.exists(): + return + p = bot.node.position + if bot.node.hurt > 0.5: + bot.handlemessage(bs.PowerupMessage('shield')) + p2 = (p[0], p[1]-0.2, p[2]) +# PopupText(random.choice(toxic),position=p2,random_offset=0.3,color=(1,0,0)).autoretain() + n = s.link_text(s, text=random.choice(toxic), bot=bot, color=(1, 0, 0)) + s.nodetimer(s, 1.5, n) + bs.timer(2, bs.Call(s.make_toxic, s, bot)) + + def phew(s, pos): + PopupText("Damage ignored", position=pos, random_offset=0.3).autoretain() + + def give_sp(s, bot): bot._punch_cooldown = 0; bot._punch_power_scale = 15; bot._super = True + + def spam_knock(s, bot): + with ga().context: + if not bot.exists() or not bot._knocked: + return + bot.node.handlemessage('knockout', 1000) + p = bot.node.position + p2 = (p[0], p[1]-0.2, p[2]) + PopupText("z", position=p2, random_offset=0.3).autoretain() + bs.timer(0.9, bs.Call(s.spam_knock, bot)) + + def spam_curse(s, bot): + with ga().context: + if not bot.exists() or not bot._cursed: + return + bot.handlemessage(bs.PowerupMessage('health')) + p2 = bot.node.position + p2 = (p2[0]+0.7, p2[1]-0.3, p2[2]) + PopupText(random.choice(nah_uh), position=p2, random_offset=0.3).autoretain() + bot.curse() + def adapter(): Nice.spam_curse(Nice, bot) + bs.timer(4.5, adapter) + + def update_alive_bots(s): + global lmao, alive_bots, alive_arr, lmao_bots, currently_dux + global on_control, move_on + alive_bots = [] + alive_arr = [] + for p in range(len(lmao)): + try: + if lmao_bots[p].node.hurt < 1: + alive_bots.append(lmao_bots[p]) + alive_arr.append(lmao[p]) + except AttributeError: + continue # vanished + + # fly override + def spaz_bot_fly(s, self): + def wrapper(b): + is_moving = abs(b.node.move_up_down) >= 0.01 or abs(b.node.move_left_right) >= 0.01 + if not b.node.exists(): + return + t = ba.apptime() + b.last_jump_time_ms = -9999 + if t - b.last_jump_time_ms >= b._jump_cooldown: + b.node.jump_pressed = True + if b.node.jump_pressed: + v = b.node.velocity + v1 = v[0] + v2 = v[1] + v3 = v[2] + p = b.node.position + p1 = p[0] + p2 = p[1] + p3 = p[2] + r = b.node.run + b.node.handlemessage("impulse", p1, 0.0+p2, p3, v1, v2, + v3, 0*r, 0*r, 0, 0, v1, v2, v3) + b.node.handlemessage("impulse", p1, 3.6+p2, p3, v1, v2, + v3, 0*r, 0*r, 0, 0, v1, v2, v3) + b.node.handlemessage('impulse', p1, p2+0.001, p3, 0, + 0.2, 0, 200, 200, 0, 0, 0, 5, 0) + b.last_jump_time_ms = t + b._turbo_filter_add_press('jump') + return wrapper + + def spaz_fly(s, _bot): + if not _bot.node.exists(): + return + _bot.node.handlemessage( + 'impulse', _bot.node.position[0], _bot.node.position[1], _bot.node.position[2], + 0.0, 0.0, 0.0, 200.0, 200.0, 0.0, 0.0, 0.0, 1.0, 0.0) + + def cola_fill(s, widget, exclude=[]): + if hasattr(widget, 'exists') and widget.exists(): + for child in widget.get_children(): + if child.get_widget_type() == 'button' and child not in exclude: + bui.buttonwidget(edit=child, color=cola) + + def bombdown(s, b=1): + global bomb_down + if b: + bomb_down = True + with ga().context: + bs.timer(0.1, bs.Call(s.bombdown, 0)) + else: + bomb_down = False + + def key(s, i): + global lmao_bots, move_on, currently_dux, bomb_control, lmao, currently_txt + global alive_bots, alive_arr, bomb_down + try: + bot = lmao_bots[currently_dux] + except IndexError: + push(f"no {currently_dux}") + with ga().context: + if i > 2: + if bomb_control and not bomb_down: + s.bombdown() + if len(alive_bots) == 1: + return # blud only has 1 bot + currently_dux += 1 + if currently_dux == len(lmao): + currently_dux = 0 + for a in range(len(lmao_bots)): + try: + if lmao_bots[currently_dux].node.hurt == 1: + currently_dux += 1 # dead + except AttributeError: + currently_dux += 1 # vanished + if currently_dux == len(lmao) + 1: + currently_dux = 0 + push(f'Switched control to {lmao[currently_dux]}', color=(0, 1, 1)) + currently_txt = f"Now controlling\n{lmao[currently_dux]}" + s.draw(currently_txt) + bui.getsound('gunCocking').play() + s.hl2(lmao_bots[currently_dux].node, True) + return + elif bomb_down: + push('too fast') + return + bot.on_bomb_press() + bot.on_bomb_release() + elif i > 1: + try: + bot.on_jump_press() + except TypeError: + bot.on_jump_press(bot) + bot.on_jump_release() + elif i: + bot.on_punch_press() + bot.on_punch_release() + else: + bot.on_pickup_press() + bot.on_pickup_release() + + def set_x(s, x): s.thex = x + + def set_y(s, y): s.they = y + + def pls_move(s): + global lmao_bots, move_on, currently_dux, move_x + global alive_bots + if s.thex and s.they: + try: + b = lmao_bots[currently_dux] + except IndexError: + return # control was stopped + except TypeError: + return # bot died lmao + b.on_move_left_right(s.they) + b.on_move_up_down(s.thex) + try: + p = b.node.position + except: + # error("an error occured, falling back and stopping control\nthis is a failsafe.") + on_control = False + allow_assign = True + s.assign() + return + if not Nice.lite_mode and Nice.animate_camera: + try: + _ba.set_camera_target(p[0], p[1], p[2]) + except UnboundLocalError: + s.draw() + bs.apptimer(0.01, s.pls_move) + + # s.welp bro lmao + def welp(s, w, nah=False, ignore=None): + if ignore: + nah = ignore + global attrs + title = 'Help' if w == 69123 else attrs[w] if w > 0 else node_attrs[-w] + desc = 'Tap on an attribute to view detailed help about it.\nI wrote this help myself by trying each,\nmay not be 100% accurate tho' if w == 69123 else welps[ + w] if w > 0 else node_welps[-w] + welp_widget = bui.containerwidget(parent=bui.get_special_widget('overlay_stack'), + size=(400, 200 if title not in [ + 'custom_name', 'gravity_scale'] else 230), + color=cola, + stack_offset=s.soff, + transition=s.anim_in, + scale=s.scale) + + bui.textwidget(parent=welp_widget, + color=(0.1, 0.7, 1), + text=title, + scale=s.scale, + h_align='center', + v_align='center', + position=(170, 150 if title not in ['custom_name', 'gravity_scale'] else 180)) + + bui.textwidget(parent=welp_widget, + text=desc if not nah else "Attribute is only editable at first spawn,\nyou can remake the bot in that case." + + ("\nThis change will be ignored." if ignore else ""), + scale=s.scale/2, + h_align='center', + v_align='center', + position=(180, 100)) + + bacc = bui.buttonwidget( + parent=welp_widget, + size=(60, 20), + label='Back', + scale=s.scale, + color=colb, + textcolor=wht, + position=(30, 30), + on_activate_call=bs.Call(s.kill, True, welp_widget, True)) + bui.containerwidget(edit=welp_widget, cancel_button=bacc) + + # checkbox manager + def check(s, n, v, mod=False): + global val_attrs2 + if mod: + val_attrs2[n] = v + else: + Nice.val_attrs[n] = v + + def tick(s, n, v): + if not n: + s.auto_spawn_on_random = v + elif n == 1: + s.random_peace = v + + # sync selection with attrs + def spawn(s, i, mod=False): + global nice_name, nice_view, val_attrs2, nice2_view, nice2_name, indox2, effect_indox, effect_tip + if mod == 1: + indox2 = i + elif mod == 2 or mod == 3: + effect_indox = i + bui.textwidget(edit=effect_tip, text=effect_tips[i], scale=0.6) + elif mod == 69: + Nice.drop_indox = i + else: + Nice.indox = i + nv = nice_view if not mod else nice2_view if mod != 69 else Nice.drop_view + va = Nice.val_attrs if not mod else val_attrs2 if mod != 69 else None + nn = nice_name if not mod else nice2_name if mod != 69 else Nice.drop_name + try: + bui.textwidget(edit=nn, text=bot_name[i] if mod not in [ + 2, 3, 69] else effect_name[i] if mod != 69 else drop_name[i]) + except: + s.spawn(i, 0) + bui.buttonwidget(edit=nv, texture=bui.gettexture( + (bot_texture[i]+'Icon') if mod not in [2, 3, 69] else effect_texture[i] if mod != 69 else drop_texture[i])) + if mod not in [2, 3, 69]: + bui.buttonwidget(edit=nv, tint_texture=bui.gettexture(bot_texture[i]+'IconColorMask')) + bui.buttonwidget(edit=nv, tint_color=va[6], tint2_color=va[11]) + va[1] = bot_name[i] + + def on_ga_change(s): + global old_ga, virgin, lmao, lmao_bots, lmao_players, lmao_bots2, testa + global LAH, LAP, LAB, LAF, lmao_chars, move_on, on_control, nood + old_ga = str(ga()) + virgin = True + print(f"Sandbox.ByBordd: Hewoo! Spawn context is now '{old_ga}'") + Nice.lmao_teams = [] + Nice.next_team_id = 2 + s.team_to_nuke = None + lmao = [] + nood = [] + lmao_bots = [] + lmao_players = [] + lmao_bots2 = [] + testa = [] + LAH = [] + LAP = [] + LAB = [] + LAF = [] + lmao_chars = [] + move_on = 0 + on_control = False + + # actual spawn + def do_spawn(s): + global cords, prev_idk, bruh, attrs, lmao, old_ga, lmao_bots, indox2 + global move_on, lmao_chars, on_control, busy + if busy: + if Nice.pause_when_bots: + push('Already spawned a bot\nResume first to spawn another\nOr Turn off pause from Config', color=( + 1, 1, 0)) + else: + push('too fast', color=(1, 1, 0)) + return + + idk = ga() + if str(idk) != old_ga: + on_ga_change() + with idk.context: + if cords is None or cords[0] == 69123: + error("Set a spawn position first") + return + busy = True + for k in ga().players: + if k.sessionplayer.inputdevice.client_id == -1: + p = k + lmao.append(random.choice(random_bot_names).replace( + '#', str(len(lmao)))) # NO_BOT) + CustomBot.set_up(attrs, Nice.val_attrs) + try: + p.customdata[lmao[-1]] = CustomBotSet(p) + p.customdata[lmao[-1]].do_custom() + except NameError: + error("You need to be in game to spawn bots") + busy = False + + # know where the hell are cords + def show_in_game(s, mode=0, mod=False): + global cords, lmao_bots, move_on, cords2 + if mod is True: + co = cords2 + elif mod is False: + co = cords + else: + co = Nice.drop_cords + if co[0] == 69123: + error("Set a position first") + return + with ga().context: + if mode: + me = s.get_my_pos() + if s.are_close(me, co, 1) == 1: + error("Join the goddamn game first") + return + elif s.are_close(me, co, 2) == 2: + error("It's right where you're standing dum") + return + elif s.are_close(me, co, 3) == 2: + bui.getsound('shieldUp').play() + push(f"No need, it's so close", color=(1, 0, 0)) + return + ding(f"Drew a line between you and position!") + if random.randint(1, 10) == 10: + push('Tip: wait for some particles to die if line wasn\'t drawn') + for i in s.draw_line(co, me): + bs.emitfx(position=i, + scale=2, count=1, spread=0, + chunk_type=chunk_types[0 if Nice.lite_mode else 1]) + else: + ding(f"Particle spawned at position!") + s.hl(co) + + # hl4 should only be called when hl3 is present + def hl4(s, bot): + def w(): + global mod_widget + return mod_widget.exists() + old = bot.node.color + old_off = (old[0]-5, old[1]-5, old[2]-5) + def hl4_off(bot, old_off): bot.node.color = old_off + + def hl4_on(bot, old): + if w(): + bot.node.color = old + # that spaz is goin blinking fr + if Nice.lite_mode: + return + bs.apptimer(0, bs.Call(hl4_on, bot, old)) + bs.apptimer(0.5, bs.Call(hl4_off, bot, old_off)) + bs.apptimer(1, bs.Call(hl4_on, bot, old)) + bs.apptimer(1.5, bs.Call(hl4_off, bot, old_off)) + bs.apptimer(2, bs.Call(hl4_on, bot, old)) + + def reset_bright_bots(s): + global lmao_bots, lmao_players + # made specially for hl3 + for b in lmao_bots: + try: + c = b.node.color + except: + continue + # this nukes all bright colors, they look annoying anyway + if c[0] >= 5: + b.node.color = (c[0]-5, c[1]-5, c[2]-5) + + try: + for b in lmao_players: + try: + c = b.actor.node.color + except: + continue + if c[0] >= 5: + b.actor.node.color = (c[0]-5, c[1]-5, c[2]-5) + except NameError: + return + + def hl3(s, i, set=True): + global lmao_bots, drux, effect_bots, lmao_players + drux = i + s.reset_bright_bots() + if Nice.lite_mode: + return + if i is not None: + try: + bot = lmao_bots[i].node if effect_bots else lmao_players[i].actor.node + except IndexError: + return + old = bot.color + if set: + bot.color = (old[0]+5, old[1]+5, old[2]+5) + + def hl2(s, p, instant=False): + s.hl3(None) + if Nice.lite_mode: + return + old = p.color + n = 10 + shade = (old[0]+n, old[1]+n, old[2]+n) + p.color = shade + + def nah(n): + n -= 0.01 + shade = (old[0]+n, old[1]+n, old[2]+n) + p.color = shade + if old[0] > shade[0]: + return + bs.apptimer(0.001, bs.Call(nah, n)) + bs.apptimer(0 if instant else 2, bs.Call(nah, n)) + + def hl(s, p): + v1 = 2 + v2 = 10 + if Nice.lite_mode: + v1 = 1 + v2 = 3 + with ga().context: + bs.emitfx(position=p, tendril_type=tendril_types[1], + scale=v1, count=v2, spread=0, + chunk_type=chunk_types[0]) + + # TODO by-id positioning + def use_my_pos(s, c2=False): + global cords, cords_widget, cords2_widget, cords2 + if c2 is True: + cords2 = s.get_my_pos() + elif c2 is False: + cords = s.get_my_pos() + else: + Nice.drop_cords = s.get_my_pos() + if s.get_my_pos(): + s.update_cords_view(c2) + else: + error('You are not in game') + s.kill(True, cords2_widget if c2 is True else cords_widget if c2 is False else s.where_drop_widget, c2) + bui.getsound('gunCocking').play() + + def get_my_pos(s): + global max_digits + p = [] + for k in ga().players: + if k.sessionplayer.inputdevice.client_id == -1: + for i in k.node.position: + p.append(float(str(i)[:max_digits])) + + cords = (float(p[0]), float(p[1]), float(p[2])) + return cords + + def update_cords_view(s, c2=False): + global cords_view, cords, cords2_view, cords2 + c = cords2 if c2 is True else cords if c2 != 69 else Nice.drop_cords + try: + bui.buttonwidget(edit=cords2_view if c2 is True else cords_view if c2 != 69 else s.drop_where, + label=f"X: {c[0]}\nY: {c[1]}\nZ: {c[2]}" if c[0] != 69123 else 'Where To\nTeleport?' if c2 != 69 else "Where To\nDeploy?") + except TypeError: + error("Join the game first bruh") + + # math is OP after all + def draw_line(s, c, me): + def gd(c, me): return ((c[0] - me[0])**2 + (c[1] - me[1])**2 + (c[2] - me[2])**2)**0.5 + d = gd(c, me) + n = int(d) + pol = [] + for i in range(n): + t = i / (n - 1) + x = c[0] + t * (me[0] - c[0]) + y = c[1] + t * (me[1] - c[1]) + z = c[2] + t * (me[2] - c[2]) + pol.append((x, y, z)) + return pol + + def are_close(s, p1, p2, sus): + try: + d = ((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2 + (p1[2] - p2[2])**2)**0.5 + except TypeError: + return 1 + if d < sus: + return 2 + else: + return 3 + + def restore_sp(s, bot): + if not bot._super: + return + if bot._has_boxing_gloves: + return + push("Suspected gloves expiration\nrestoring super punch") + s.give_sp(bot) + + +class NicePick(bui.Window): + def __init__(s, t, u, i): + global hmm + hmm = i + ColorPicker( + parent=bui.get_special_widget('overlay_stack'), + tag=('color'), + delegate=s, + initial_color=u, + position=(700, 0)) + + def _set_color(s, color): pass + + def color_picker_selected_color(s, picker, c): + global hmm, val_attrs2, ins + Nice.val_attrs[hmm] = c + bui.buttonwidget(edit=ins[hmm], + label=f"{str(c[0]+0.01)[:3]} {str(c[1]+0.01)[:3]}, {str(c[2]+0.01)[:3]}", + color=c, + on_activate_call=bs.Call(NicePick, s, c, hmm), + textcolor=(1-c[0], 1-c[1], 1-c[2])) + Nice.gather(Nice, False, False) + val_attrs2[hmm] = c + + def color_picker_closing(self, picker): pass + + +class NicePick2(bui.Window): + def __init__(s, t, u, i): + global hmm2 + hmm2 = i + ColorPicker( + parent=bui.get_special_widget('overlay_stack'), + tag=('color'), + delegate=s, + initial_color=u, + position=(700, 0)) + + def _set_color(s, color): pass + + def color_picker_selected_color(s, picker, c): + global hmm2, val_attrs2, ins2 + val_attrs2[hmm2] = c + bui.buttonwidget( + edit=ins2[hmm2], label=f"{str(c[0]+0.01)[:3]} {str(c[1]+0.01)[:3]}, {str(c[2]+0.01)[:3]}", color=c, textcolor=(1-c[0], 1-c[1], 1-c[2])) + bui.buttonwidget(edit=ins2[hmm2], on_activate_call=bs.Call(NicePick2, s, c, hmm2)) + + def color_picker_closing(self, picker): pass + + +class PickerLight(bui.Window): + def __init__(s, u): + ColorPicker( + parent=bui.get_special_widget('overlay_stack'), + tag=('color'), + delegate=s, + initial_color=u, + position=(700, 0)) + + def _set_color(s, color): pass + + def color_picker_selected_color(s, picker, c): + bui.buttonwidget(edit=light_pick, color=c) + bui.buttonwidget(edit=light_pick, textcolor=Nice.negate(Nice, c)) + bui.buttonwidget(edit=light_pick, on_activate_call=bs.Call(PickerLight, c)) + Nice.ga_tint = c + + def color_picker_closing(self, picker): pass + + +class PickerLol(bui.Window): + def __init__(s, u): + ColorPicker( + parent=bui.get_special_widget('overlay_stack'), + tag=('color'), + delegate=s, + initial_color=u, + position=(700, 0)) + + def _set_color(s, color): pass + + def color_picker_selected_color(s, picker, c): + Nice.LTWAC = c + bui.buttonwidget(edit=LTWAB, color=c) + bui.buttonwidget(edit=LTWAB, textcolor=Nice.negate(Nice, c)) + bui.buttonwidget(edit=LTWAB, on_activate_call=bs.Call(PickerLol, Nice.LTWAC)) + + def color_picker_closing(self, picker): pass + + +class Picker(popup.PopupWindow): + def __init__(s, mod=0): + uiscale = bui.app.ui_v1.uiscale + scale = (1.9 if uiscale is ba.UIScale.SMALL else 1.6 if uiscale is ba.UIScale.MEDIUM else 1) + count = len(bot_texture) if mod not in [2, 69] else len( + effect_texture) if mod != 69 else len(drop_texture) + columns = 3 + rows = int(math.ceil(float(count) / columns)) + bw = 100 + bh = 100 + bbh = 10 + bbv = 15 + s._width = (10 + columns * (bw + 2 * bbh) * (1.0 / 0.95) * (1.0 / 0.8)) + s._height = s._width * 0.8 + s._sw = s._width * 0.8 + s._sh = s._height * 0.9 + s._sp = ((s._width - s._sw) * 0.5, (s._height - s._sh) * 0.5) + popup.PopupWindow.__init__(s, + position=(550.0, 0.0), + size=(s._width, s._height), + scale=scale, + bg_color=(0, 0, 0), + focus_position=s._sp, + focus_size=(s._sw, s._sh)) + s._scrollwidget = bui.scrollwidget(parent=s.root_widget, + size=(s._sw, s._sh), + color=(0, 0, 0), + position=s._sp) + bui.containerwidget(edit=s._scrollwidget, claims_left_right=True) + s._sub_width = s._sw * 0.95 + s._sub_height = 5 + rows * (bh + 2 * bbv) + 100 + s._subcontainer = bui.containerwidget(parent=s._scrollwidget, size=( + s._sub_width, s._sub_height), background=False) + bui.textwidget(parent=s.root_widget, + text='Select character (scroll)' if mod != 2 and mod != 69 else 'Select effect (scroll)' if mod != 69 else 'What to deploy? (scroll)', + scale=scale/2, + position=(130, 364)) + mask_texture = bui.gettexture('characterIconMask') # good frame + index = 0 + for y in range(rows): + for x in range(columns): + pos = (x * (bw + 2 * bbh) + bbh, s._sub_height - (y + 1) * (bh + 2 * bbv) + 12) + try: + icon = bui.gettexture(bot_texture[index] + 'Icon') if mod not in [2, 69] else bui.gettexture( + effect_texture[index]) if mod != 69 else bui.gettexture(drop_texture[index]) + except IndexError: + return + btn = bui.buttonwidget( + parent=s._subcontainer, + button_type='square', + position=(pos[0], pos[1]), + size=(bw, bh), + autoselect=True, + texture=icon, + tint_texture=(bui.gettexture( + bot_texture[index]+'IconColorMask') if mod not in [2, 69] else None), + tint_color=val_attrs2[6] if mod == 1 else None if mod in [ + 2, 69] else Nice.val_attrs[6], + tint2_color=val_attrs2[11] if mod == 1 else None if mod in [ + 2, 69] else Nice.val_attrs[11], + color=(1, 1, 1), + mask_texture=mask_texture, + label='', + on_activate_call=bs.Call(s.ok, index, mod)) + bui.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + name = bot_name[index] if mod not in [ + 2, 69] else effect_name[index] if mod != 69 else drop_name[index] + bui.textwidget(parent=s._subcontainer, + text=name, + position=(pos[0] + bw * 0.5, pos[1] - 12), + size=(0, 0), + scale=0.5, + maxwidth=bw, + draw_controller=btn, + h_align='center', + v_align='center') + index += 1 + if index >= len(bot_texture if mod not in [2, 69] else effect_texture if mod != 69 else drop_texture): + break # brb + if index >= len(bot_texture if mod not in [2, 69] else effect_texture if mod != 69 else drop_texture): + break # bye bye + + def ok(s, index, mod=False): + global effect_bots + bui.containerwidget(edit=s.root_widget, transition=anim_out) + if index or index == 0: + Nice.spawn(Nice, index, mod if effect_bots or mod == 69 else mod + 1) + + def on_popup_cancel(s) -> None: + bui.getsound('swish').play() + s.ok(None) + + +class TexturePicker(popup.PopupWindow): + def __init__(s): + scale = Nice.scale + count = len(all_texture) + columns = 6 + rows = int(math.ceil(float(count) / columns)) + bw = 100 + bh = 100 + bbh = 10 + bbv = 15 + s._width = (10 + columns * (bw + 2 * bbh) * (1.0 / 0.95) * (1.0 / 0.8)) + s._height = s._width * 0.8 + s._sw = s._width * 0.8 + s._sh = s._height * 0.9 + s._sp = ((s._width - s._sw) * 0.5, (s._height - s._sh) * 0.5) + popup.PopupWindow.__init__(s, + position=(550.0, 0.0), + size=(s._width, s._height), + scale=scale, + bg_color=(0, 0, 0), + focus_position=s._sp, + focus_size=(s._sw, s._sh)) + s._scrollwidget = bui.scrollwidget(parent=s.root_widget, + size=(s._sw, s._sh), + color=(0, 0, 0), + position=s._sp) + bui.containerwidget(edit=s._scrollwidget, claims_left_right=True) + s._sub_width = s._sw * 0.95 + s._sub_height = 5 + rows * (bh + 2 * bbv) + 100 + s._subcontainer = bui.containerwidget(parent=s._scrollwidget, size=( + s._sub_width, s._sub_height), background=False) + index = 0 + for y in range(rows): + for x in range(columns): + pos = (x * (bw + 2 * bbh) + bbh, s._sub_height - (y + 1) * (bh + 2 * bbv) + 12) + try: + icon = bui.gettexture(all_texture[index]) + except IndexError: + return + btn = bui.buttonwidget( + parent=s._subcontainer, + button_type='square', + position=(pos[0], pos[1]), + size=(bw, bh), + autoselect=True, + texture=icon, + color=(1, 1, 1), + label='', + on_activate_call=bs.Call(s.ok, index)) + bui.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + name = all_texture[index] + bui.textwidget(parent=s._subcontainer, + text=name, + position=(pos[0] + bw * 0.5, pos[1] - 12), + size=(0, 0), + scale=0.5, + maxwidth=bw, + draw_controller=btn, + h_align='center', + v_align='center') + index += 1 + if index >= len(all_texture): + break + if index >= len(all_texture): + break # bye bye + + def ok(s, index): + global THE_TB + bui.containerwidget(edit=s.root_widget, transition=anim_out) + try: + bui.textwidget(edit=THE_TB, text=all_texture[index]) + except TypeError: + pass # NoneType + + def on_popup_cancel(s) -> None: + bui.getsound('swish').play() + s.ok(None) + + +# Initialize Arrays +toxic = ["You can't run too far", + "You're dead", + "Keep running noob", + "Come here you deadmeat", + "It's about time", + "Do not resist your death", + "The instrument of doom", + "Your death is near", + "I see your fear, just die", + "COME HERE", + "KILL KILL KILL", + "I'm the OnePunchMan", + "STOP LET ME KILL U", + "YOU'RE ALMOST DEAD"] + +toxic_win = ["HAHAHHAA", + "Easy noob", + "And stay dead", + "That was easy", + "HE'S DEAD.", + "YOU FAILED! HAHAH"] + +load_name = ["Beboo The GOAT", + "Kronk Buddy", + "Flying Pixel", + "Suicidal Jack (or not?)", + "Big Shiny TNT", + "Liying bomb", + "Huge Safe Mine"] + +random_team = [ + ["Cyan", (0, 1, 1)], + ["Yellow", (1, 1, 0)], + ["Winners", (1, 0, 1)], + ["Zamn", (0.5, 1, 0.5)], + ["Gang", (0.5, 0.5, 1)], + ["White", (1, 1, 1)], + ["Noobs", (0.4, 0.2, 0.1)], + ["Pros", (0.2, 0.4, 0.1)], + ["Green", (0, 1, 0)], + ["Orange", (1, 0.5, 0)], + ["Purple", (0.5, 0, 0.5)], + ["Silver", (0.75, 0.75, 0.75)], + ["Gold", (0.8, 0.6, 0.2)], + ["Pink", (1, 0.5, 0.5)], + ["Turquoise", (0, 1, 1)], + ["Lime", (0.75, 1, 0)], + ["Maroon", (0.5, 0, 0)], + ["Teal", (0, 0.5, 0.5)], + ["Navy", (0, 0, 0.5)], + ["Magenta", (1, 0, 1)], + ["Brown", (0.6, 0.3, 0)], + ["Sky", (0.53, 0.81, 0.92)], + ["Raptors", (0.9, 0.1, 0.1)], + ["Sharks", (0.1, 0.1, 0.9)], + ["Tigers", (1, 0.5, 0)], + ["Dragons", (0.5, 0, 1)], + ["Falcons", (0.75, 0.75, 0)], + ["Wolves", (0.5, 0.5, 0.5)], + ["Lions", (1, 0.5, 0.5)], + ["Panthers", (0.1, 0, 0.1)] +] + +node_attrs = ['gotta clean this code soon', 'gravity_scale', 'sticky', + 'reflection=\'powerup\'', "reflection='soft'", "reflection_scale"] +node_welps = ["i mean for real", "How likely is it to be pulled by the ground.\ndefault: 1.0, increasing makes it heavier\ndecreasing makes it lighter, it may even fly,\nnegative values cause object to fall up", + 'When checked, object spawns sticky\nwhich sticks to anything lol', "reflect light like powerups do", "reflect light softly like bombs do", "how shiny the reflection can get, default is 1.2\nsetting to something like 40 results in\na very shiny object which looks cool"] +powerup_name = ['triple_bombs', 'curse', 'health', 'ice_bombs', + 'impact_bombs', 'land_mines', 'punch', + 'shield', 'sticky_bombs'] +effect_texture = (['powerupBomb', 'powerupCurse', 'powerupHealth', 'powerupIceBombs', + 'powerupImpactBombs', 'powerupLandMines', 'powerupPunch', + 'powerupShield', 'powerupStickyBombs', 'graphicsIcon', + 'bombColorIce', 'touchArrowsActions', 'trophy', + 'crossOut', 'bonesIcon', 'lock', + 'achievementGotTheMoves', 'backIcon', + 'star', 'achievementCrossHair', + 'achievementOffYouGo', 'achievementFootballShutout', + 'achievementSuperPunch', 'leftButton', + "buttonJump", "downButton"]) +effect_texture.append("neoSpazIconColorMask") +effect_texture.append("replayIcon") + +effect_name = ["Triple Bombs", "Curse", "Heal", "Ice Bombs", "Impact Bombs", "Land Mines", + "Gloves", "Energy Shield", "Sticky Bombs", "Shatter", "Freeze", + "Unfreeze", "Celebrate", "Stop Celebrating", "Kill", "Infinite Curse", + "Super Speed", "Normal Speed", "Invincible", "Beatable", "Sleep", + "Wake Up", "Super Punch", "Normal Punch", "Fly Jumps", "Normal Jumps"] +effect_name.append("GodMode Preset") +effect_name.append("Reset All") + +effect_tips = ["PowerUp\nSets default_bomb_count\nvalue to 3 for a short while", + "PowerUp\nCurse the player, making\nthem explode in 5 seconds", + "PowerUp\nHeal the player, removing\nany curses and wounds!", + "PowerUp\nSets default_bomb_type\nvalue to ice for a short while", + "PowerUp\nSets default_bomb_type\nvalue to impact for a short while", + "PowerUp\nSets land_mine_count\nvalue to 3, decreased on use", + "PowerUp\nGives the player\nboxing gloves for a short while", + "PowerUp\nGives the player an\nenergy shield, decays over time", + "PowerUp\nSets default_bomb_type\nvalue to sticky for a short while", + "Effect\nShatters the player\ntearing them apart everywhere", + "Effect\nFreezes the player\nparalyzing them for an amount of time\nand calling FreezeMessage\ndoesn't work on shielded spazes", + "Effect\nImmediately unfreezes player\nmelting the ice and calling ThawMessage", + "Emote\nMakes the spaz celebrate forever!", + "Emote\nMakes the spaz stop celebrating!", + "Instant Effect\nImmediately kill the spaz\nfor good.", + "Effect Granter\nApply an infinite curse\non the poor spaz, although\ncurse -> heal -> repeat", + "Effect Granter\nApply super speed to bot\nsince I couldn't make it run\nbcz me nub", + "Effect Revoker\nReturn bot speed to normal\nin case you used super speed", + "Effect Granter\nMake the bot Invincible!\nwhich means it would be immune,\nmaking a weird rod sound upon\nbeing hit", + "Effect Revoker\nTake the invincibility off the bot\nmaking it valnurable", + "Effect Granter\nMake the bot have a nap that lasts forever.\nYes, they won't wake up", + "Effect Revoker\nWake the bot up if it's sleeping or so", + "Effect Granter\nTurn the bot into OnePunchMan\nin addition to removing\npunch cooldown", + "Effect Revoker\nRemoves super punch powers from bot", + "Effect Granter\nGives the bot unlimited jumps\nwhich makes it fly after each", + "Effect Revoker\nRemoves the fly jumps effect\nwhich returns old jumping behaviour"] +effect_tips.append("Multiple Effect Granter\nGiven effects:\nSuper Punch\nInvincibility\nSpeed") +effect_tips.append("Universal Effect Revoker\nRemoves all effects on bot,\nwho's laughing now?") + +effect_message = ["triple_bombs", "curse", "health", "ice_bombs", "impact_bombs", + "land_mines", "punch", "shield", "sticky_bombs"] + +bot_texture = (['neoSpaz', 'kronk', 'zoe', 'ninja', 'mel', 'jack', 'bunny', + 'agent', 'penguin', 'cyborg', 'pixie', 'frosty', 'wizard', + 'bear', 'ali', 'santa', 'bones']) + +bot_name = (['Spaz', 'Kronk', 'Zoe', 'Snake Shadow', 'Mel', 'Jack Morgan', 'Easter Bunny', + 'Agent Johnson', 'Pascal', 'B-9000', 'Pixel', 'Frosty', + 'Grumbledorf', 'Bernard', 'Taobao Mascot', 'Santa Claus', 'Bones']) + +bot_style = ["spaz", "kronk", "female", "ninja", "mel", "pirate", + "bunny", "agent", "penguin", "cyborg", "pixie", + "frosty", "wizard", "bear", "ali", "santa", "bones"] + +w_bot_name = ['Spas', 'Kornk', 'Girl', 'Ninja', 'Cook', 'Jack', 'Rabbit', 'Agent', 'Penguin', + 'Robot', 'Angel', 'Snowman', 'Wizard', 'Bear', 'Santa', 'Skeleton'] + +indox2 = 0 +effect_dux = 0 +effect_bots = True +has_no_color = ["kronk"] +has_no_style = ["wizard"] +on_control = False +virgin = True +bomb_down = False +effect_dux2 = 0 +drux = 0 +cords = (69123, 0, 0) +cords2 = (69123, 0, 0) +chunk_types = ['spark', 'slime', 'ice', 'metal', 'sweat', 'splinter', 'rock'] +emit_types = ['flag_stand', 'distortion', 'stickers', 'tendrils'] +tendril_types = ['ice', 'smoke', 'thin_smoke'] +max_digits = 8 +nice_custom_text = '$' +nice_custom_color = (0.7, 0.7, 0.7) +attrs = ["bouncy", "character", "charge_dist_max", "charge_dist_min", + "charge_speed_max", "charge_speed_min", "color", "default_bomb_count", + "default_bomb_type", "default_boxing_gloves", "default_shields", "highlight", "punchiness", "run", "run_dist_min", + "demo_mode", "static", "throw_dist_max", "throw_dist_min", "throw_rate", + "throwiness", "can_accept_powerups", "start_invincible", 'attack_host', 'attack_players', + 'attack_bots', 'attack_your_bots', 'custom_name', 'custom_name_color'] +not_editable = [21, 22] +music_name = ["Char Select", "Chosen One", "Epic", "Epic Race", "Flag Catcher", + "Flying", "Football", "Forward March", "Grand Romp", "Hockey", + "Keep Away", "Marching", "Menu", "Onslaught", "Race", + "Runaway", "Scary", "Scores", "Sports", "Survival", + "To The Death", "Victory"] + +drop_texture = (['powerupBomb', 'powerupCurse', 'powerupHealth', 'powerupIceBombs', + 'powerupImpactBombs', 'powerupLandMines', 'powerupPunch', + 'powerupShield', 'powerupStickyBombs', 'tnt', + "landMine", "landMineLit", "eggTex1", "eggTex2", "eggTex3", "white", "black", + "bombColor", "impactBombColor", "bombStickyColor", "bombColorIce"]) +drop_name = ["Triple Bombs", "Curse", "Heal", "Ice Bombs", "Impact Bombs", "Land Mines", + "Gloves", "Energy Shield", "Sticky Bombs", 'TNT', 'Land Mine', + "Lit Land Mine", "Striped Egg", "Lined Egg", "Dotted Egg", "White Egg", "Black Egg", + "Bomb", "Impact Bomb", "Sticky Bomb", "Ice Bomb"] + +music_texture = ["neoSpazIcon", "achievementSuperPunch", "tipTopPreview", "bigGPreview", "bridgitPreview", + "alwaysLandPreview", "achievementFootballVictory", "cragCastlePreview", "achievementFlawlessVictory", "hockeyStadiumPreview", + "thePadPreview", "achievementRunaround", "logo", "doomShroomPreview", "lakeFrigidPreview", + "monkeyFacePreview", "powerupCurse", "achievementFootballShutout", "footballStadiumPreview", "rampagePreview", + "achievementOnslaught", "achievementMedalLarge"] +music_desc = ["charSelectMusic.ogg", "survivalMusic.ogg", "slowEpicMusic.ogg", "slowEpicMusic.ogg", "flagCatcherMusic.ogg", + "flyingMusic.ogg", "sportsMusic.ogg", "forwardMarchMusic.ogg", "grandRompMusic.ogg", "sportsMusic.ogg", + "runAwayMusic.ogg", "whenJohnnyComesMarchingHomeMusic.ogg", "menuMusic.ogg", "runAwayMusic.ogg", "runAwayMusic.ogg", + "runAwayMusic.ogg", "scaryMusic.ogg", "scoresEpicMusic.ogg", "sportsMusic.ogg", "survivalMusic.ogg", + "toTheDeathMusic.ogg", "victoryMusic.ogg"] +music_type = [bs.MusicType.CHAR_SELECT, + bs.MusicType.CHOSEN_ONE, + bs.MusicType.EPIC, + bs.MusicType.EPIC_RACE, + bs.MusicType.FLAG_CATCHER, + bs.MusicType.FLYING, + bs.MusicType.FOOTBALL, + bs.MusicType.FORWARD_MARCH, + bs.MusicType.GRAND_ROMP, + bs.MusicType.HOCKEY, + bs.MusicType.KEEP_AWAY, + bs.MusicType.MARCHING, + bs.MusicType.MENU, + bs.MusicType.ONSLAUGHT, + bs.MusicType.RACE, + bs.MusicType.RUN_AWAY, + bs.MusicType.SCARY, + bs.MusicType.SCORES, + bs.MusicType.SPORTS, + bs.MusicType.SURVIVAL, + bs.MusicType.TO_THE_DEATH, + bs.MusicType.VICTORY] +val_attrs2 = Nice.def_attrs.copy() # for modifying, not creating +val_arr = [] +type_attrs = [type(i).__name__ for i in Nice.def_attrs] +lmao = [] # list of bots by string +testa = [] # list of bot floating names +old_ga = str(ga()) +LAH = [] +LAP = [] +LAB = [] +LAF = [] +ATK = ['attack_host', 'attack_players', 'attack_bots', 'attack_your_bots'] +lmao_bots = [] +lmao_players = [] +lmao_bots2 = [] +lmao_chars = [] +move_on = 0 +dux = None +kon = None +do_tp = True +cola = (0.13, 0.13, 0.13) +colb = cola +wht = (1, 1, 1) +busy = False +# colb = (0.2, 0.2, 0.2) +anim_in = 'in_right' +anim_out = 'out_right' +bomb_type = ['normal', 'impact', 'sticky', 'ice', 'land_mine', 'tnt'] +w_bomb_type = ['default', 'black', 'green', 'blue', 'mine', 'box'] +welps = ['When checked, the bot randomly jumps around,\nincreasing its damage', + 'The way bot looks, more like of its "skin"\nIt has nothing to do with behavior', + 'How close the bot needs to get to you\nbefore attempting to bomb you', + 'How far the bot needs to get away from you\nbefore attempting to bomb you', + 'The limit of bomb reload speed which the bot\ncan\'t go any faster', + 'The limit of bomb reload speed which the bot\ncan\'t go any slower', + 'The main color of the bot, Has nothing to do\nwith its behaviour', + 'How much bombs the bot is allowed to throw\nin a row before the old ones explode.\nBlud has the triple bomb by default rip', + 'The type of the bomb which the bot throws,\navailable types are\nnormal, sticky, impact, ice, tnt and land_mine', + 'When checked, the bot will spawn with a shield.\nUnfair naa?', + 'When checked, the bot will spawn with gloves.\nClick spawn and start running away.', + 'The side color of bot, covers places which the\ncolor attribute doesn\'t', + 'How likely is the bot to punch, simply\nincrease this enough and it\'s gonna spam punch lol', + 'When checked, the bot is allowed to run,\nincreasing its damage', + 'How far the bot needs to be to start running', + 'I have no idea what is this,\npreviously this was start_cursed but\nI removed it because it was annoying', + 'When checked, the bot will not try to follow you,\ninstead, will try to bomb you from a remote distance.\nSpaz\'es in Rookie Onslaught have this by default.', + 'How far the distance needed by bot\nbefore throwing a bomb\nsimilar to charge, but throw!', + 'How close the distance needed\nso the bot stops throwing', + 'How likely is the bot to spam bombs', + 'How pro the bot can be at predicting your next move\nand throwing a bomb at a place you would be in', + "When checked, bot can collect powerups!", + "hen checked, bot spawns invincible for a short while", + "When checked, the bot attacks the game host,\nother players have another check", + "When checked, the bot attacks all players\nexcept the host, which has his own check", + "When checked, the bot attacks other bots,\nexcept its friends, the bots that\nYOU spawned.", + "When checked, the bot betrays other bots\nthat YOU spawned, for bots that you\ndidn\'t spawn exists another check", + "Gives the bot a nice name!\nBot mame appears above its head.\nSet to '%' to follow default name,\nset to '$' to follow bot's control codename", + "The color of bot's custom_name"] + +# For Nice.what_is +what_is_arr = [["tnt", "TNT"], + ["powerup", "PUP"]] + +# BotName Builder +random_bot_names = [ + "Gapple#", "iPun#", "BathRom#", "double#", "Times#", "InsertName#", "ShutUp#", "Botty#", "Bottie#", "Clumsy#", + "Cheeky#", "Phucc#", "Cope#", "Bebo#", "Sike#", "AwMan#", "Putt#", "Nuts#", "Kids#", "Poo#", + "Bang#", "Sus#", "OnCrack#", "Cadeau#", "Bureau#", "Yasta#", "Eshta#", "YaZmele#", "7abibo#", "Straight#", + "Egg#", "NotEgg#", "MaybeEgg#", "ProbEgg#", "YouEgg#", "YouNoob#", "Nub#", "HahaNoob#", "LMAO#", "LOL#", + "Bomb#", "FrBro?#", "ForReal#", "RealOrFake#", "Real#", "Fake#", "UnFake#", "Realn't#", "Bruh#", "Stop#", + "SnapDrag#", "Exynos#", "Lagsynos#", "GnuNano#", "Lynx#", "How#", "TeachMe#", "Scam#", "Cap#", "ScamSung#", + "iBot#", "Just#", "I'mNot#", "Run#", "EzNoob#", "GoSleep#", "Pain#", "GalaxyTabA#", "GalaxyA#", "GalaxyS#", + "GalaxyM#", "ReleasedV#", "MtkGen#", "MediaTek#", "IntelI#", "IndexIs#", "MyOrder#", "Grep#", "KaliLinux#", "Bios#", + "Ftw#", "Tldr#", "Simp#", "MrSmoth#", "Bordd#", "Geh#", "KillMe#", "Bruda#", "Otg#" +] + +# infinite curse pop ups +nah_uh = ["naah uh", "not today", "reset that counter!", "I'm stayin alive", + "not in mood\nto explood", "Not today,\nthank you.", + "Let that 1\nbecome a 5", "boomn't, let's\ntry again", + "Infinite curse\non duty!", "3.. 2.. 1.. repeat!", "this takes forever", + "nope, try again", "The power of\nthe infinite curse!"] + +for i in range(len(nah_uh)): + nah_uh.append("") # 50% chance to say nothing + + +def meow_patch(og): + def wrapper(self, msg: Any) -> Any: + # print(msg) + if isinstance(msg, bs.DieMessage): + if msg.how == bs.DeathType.IMPACT and self.node.getdelegate(object) not in Nice.toxic_bots: + Nice.toxic_celebrate(Nice) + global on_control + if msg.how == bs.DeathType.IMPACT and hasattr(self.node.getdelegate(object), 'source_player') and on_control: + on_control = False + Nice.assign(Nice) + push('Control will stop when your corpse vanish LOL', color=(0, 1, 1)) + elif isinstance(msg, bs.ImpactDamageMessage) and hasattr(self.node.getdelegate(object), 'source_player') and self.node.getdelegate(object).node.invincible: + pos = self.node.getdelegate(object).node.position + with ga().context: + bs.timer(0.001, bs.Call(Nice.phew, Nice, pos)) + return + elif isinstance(msg, bs.PowerupMessage) and msg.poweruptype == 'punch' and hasattr(self.node.getdelegate(object), "_super") and self.node.getdelegate(object)._super: + if on_control: + push('Gloves have canceled your Super Punch effect\nDon\'t worry, restoring Super Punch') + bot = self.node.getdelegate(object) + with ga().context: + bs.timer(0.01, bs.Call(Nice.give_sp, Nice, bot)) + bs.timer(20.1, bs.Call(Nice.restore_sp, Nice, bot)) + try: + return og(self, msg) + except: + pass # safe mines are gay + return wrapper + + +Spaz.handlemessage = meow_patch(Spaz.handlemessage) + + +class CustomBot(SpazBot): + @classmethod + def set_up(cls, attrs, val_attrz): + global LAH, LAP, LAB, LAF, nice_custom_text, nice_custom_color, lmao, lmao_chars, val_arr + val_arr.append(val_attrz.copy()) + dict = {attrs[i]: val_attrz[i] for i in range(len(attrs))} + for key, value in dict.items(): + if key == 'character': + lmao_chars.append(value) + if key in ATK: + if key == ATK[0]: + LAH.append(value) + if key == ATK[1]: + LAP.append(value) + if key == ATK[2]: + LAB.append(value) + if key == ATK[3]: + LAF.append(value) + elif key == 'custom_name': + t = value + if value == '%': + t = val_attrz[attrs.index('character')] + elif value == '$': + t = lmao[-1] # NO_BOT + nice_custom_text = t + elif key == 'custom_name_color': + nice_custom_color = value + else: + setattr(cls, key, value) + + def __init__(self, player) -> None: + Spaz.__init__(self, + color=self.color, + highlight=self.highlight, + character=self.character, + source_player=None, + start_invincible=self.start_invincible, + can_accept_powerups=self.can_accept_powerups) + self.update_callback: Optional[Callable[[SpazBot], Any]] = None + activity = self.activity + assert isinstance(activity, bs.GameActivity) + self._map = weakref.ref(activity.map) + self.last_player_attacked_by: Optional[bs.Player] = None + self.last_attacked_time = 0.0 + self.last_attacked_type: Optional[Tuple[str, str]] = None + self.target_point_default: Optional[bs.Vec3] = None + self.held_count = 0 + self.last_player_held_by: Optional[bs.Player] = None + self.target_flag: Optional[Flag] = None + self._charge_speed = 0.5 * (self.charge_speed_min + + self.charge_speed_max) + self._lead_amount = 0.5 + self._mode = 'wait' + self._charge_closing_in = False + self._last_charge_dist = 0.0 + self._running = False + self._last_jump_time = 0.0 + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, bs.DieMessage): + if self.node.getnodetype() == 'spaz': + s = self.node.getdelegate(object) + if isinstance(s, SpazBot) and s in lmao_bots: + try: + j = lmao_bots.index(s) + except: + j = 69123 + if j != 69123: + Nice.update_alive_bots(Nice) + global move_on, dux, control_widget, on_control, allow_assign, alive_bots, currently_dux + if not s._dead: + move_on += 1 + if Nice.notify_bot_ded: + push(f'{lmao[lmao_bots.index(s)]} has died!', color=(1, 0, 1)) + p = self.node.position # PEPSI + if dux == lmao_bots.index(s) and on_control: + push('the bot you are controlling has died LMAO', color=(1, 0.2, 0.7)) + on_control = False + allow_assign = True + Nice.assign(Nice) + wasdead = self._dead + self._dead = True + self.hitpoints = 0 + if msg.immediate: + if self.node: + self.node.delete() + elif self.node: + self.node.hurt = 1.0 + if self.play_big_death_sound and not wasdead: + SpazFactory.get().single_player_death_sound.play() + self.node.dead = True + bs.timer(2.0, self.node.delete) # TODO ragdoll erase time settings + else: + return super().handlemessage(msg) + + +class CustomBotSet(SpazBotSet): + def __init__(self, + source_player: bs.Player = None) -> None: + self._bot_list_count = 5 + self._bot_add_list = 0 + self._bot_update_list = 0 + self._bot_lists: List[List[SpazBot]] = [ + [] for _ in range(self._bot_list_count) + ] + self._spawn_sound = bs.getsound('spawn') + self._spawning_count = 0 + self._bot_update_timer: Optional[bs.Timer] = None + self.start_moving_customs() + self.source_player = source_player + + def do_custom(self) -> None: + global cords + self.spawn_bot(CustomBot, + cords, + 0, self.setup_custom) + + def start_moving_customs(self) -> None: + self._bot_update_timer = bs.Timer(0.05, + bs.WeakCall(self._bupdate), + repeat=True) + + def _spawn_bot(self, bot_type: type[SpazBot], pos: Sequence[float], + on_spawn_call: Optional[Callable[[SpazBot], Any]]) -> None: + spaz = bot_type(self.source_player) + self._spawn_sound.play(position=pos) + spaz.node.handlemessage('flash') + spaz.node.is_area_of_interest = True + spaz.handlemessage(bs.StandMessage(pos, random.uniform(0, 360))) + self.add_bot(spaz) + if on_spawn_call is not None: + on_spawn_call(spaz) + global busy + busy = False + + def _bupdate(self) -> None: + global LAH, LAP, LAB, LAF, lmao_bots + nuds = bs.getnodes() + bot_list = self._bot_lists[self._bot_update_list] = ([ + b for b in self._bot_lists[self._bot_update_list] if b + ]) + player_pts = [] + not_host = [] + bad_bots = [] + good_bots = [] + host = None + for n in nuds: + if n.getnodetype() == 'spaz': + s = n.getdelegate(object) + if isinstance(s, SpazBot): + if not s in self.get_living_bots(): + if hasattr(s, 'source_player'): + player_pts.append(( + bs.Vec3(n.position), + bs.Vec3(n.velocity))) + if s.source_player is self.source_player: + good_bots.append(player_pts[-1]) + else: + player_pts.append(( + bs.Vec3(n.position), + bs.Vec3(n.velocity))) + bad_bots.append(player_pts[-1]) + elif isinstance(s, PlayerSpaz): + player_pts.append(( + bs.Vec3(n.position), + bs.Vec3(n.velocity))) + bowl = s.getplayer(bs.Player, True) is self.source_player + if bowl: + host = player_pts[-1] + else: + not_host.append(player_pts[-1]) + + for bot in bot_list: + if bot not in lmao_bots: + lmao_bots.append(bot) + i = lmao_bots.index(bot) + if not LAH[i]: + player_pts.remove(host) + if not LAP[i]: + player_pts = [k for k in player_pts if k not in not_host] + if not LAB[i]: + player_pts = [k for k in player_pts if k not in bad_bots] + if not LAF[i]: + player_pts = [k for k in player_pts if k not in good_bots] + bot.set_player_points(player_pts) + bot.update_ai() + + for bot in self.get_living_bots(): + if bot not in lmao_bots2 and bot not in lmao_bots: + lmao_bots2.append(bot) + + def setup_custom(self, spaz) -> None: + spaz.source_player = self.source_player + self.set_custom_text(spaz) + for i in Nice.pending: + if i == "sp": + Nice.give_sp(Nice, spaz) + if i == "speed": + spaz.node.hockey = True + if i == "constant_heal": + Nice.constant_heal(Nice, spaz) + if i == "toxic": + Nice.toxic_bots.append(spaz) + Nice.make_toxic(Nice, spaz) + if i == "constant_jump": + spaz.on_jump_press = Nice.spaz_bot_fly(Nice, spaz.on_jump_press) + Nice.constant_jump(Nice, spaz) + if i == "infinite_curse": + if spaz._cursed: + spaz.handlemessage(bs.PowerupMessage('health')) + spaz.curse() + Nice.spam_curse(Nice, spaz) + + def set_custom_text(self, spaz) -> None: # FLOAT + global nice_custom_text, nice_custom_color, testa + try: + m = bs.newnode('math', + owner=spaz.node, + attrs={'input1': (0, 1.2, 0), + 'operation': 'add'}) + spaz.node.connectattr('position', m, 'input2') + test = spaz._custom_text = bs.newnode( + 'text', + owner=spaz.node, + attrs={'text': nice_custom_text, + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': nice_custom_color, + 'scale': 0.0, + 'h_align': 'center'}) + m.connectattr('output', spaz._custom_text, 'position') + bs.animate(spaz._custom_text, 'scale', {0: 0.0, 0.5: 0.01}) + testa.append(test) + except: + pass + +# ba_meta require api 9 +# BroBordd touch grass +# Copyright 2024, solely by BroBordd. All rights reserved. + +# ba_meta export babase.Plugin + + +class byBordd(ba.Plugin): + def __init__(s): + igm._refresh_in_game = Nice.Button(igm._refresh_in_game) + + +# All Textures (generated) +all_texture = [i[:-4] for i in ls("ba_data/textures")] diff --git a/plugins/utilities/server_switch.py b/plugins/utilities/server_switch.py new file mode 100644 index 000000000..2dbc667f0 --- /dev/null +++ b/plugins/utilities/server_switch.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# ba_meta require api 8 +''' +Server Switch Plugin by My.Smoothy +Let you switch recently joined servers very quickly ++ Added button to quicky look into public server list without leaving current game. + +discord: mr.smoothy +https://discord.gg/ucyaesh +Youtube : Hey Smoothy +Download more mods from +https://bombsquad-community.web.app/mods +''' +import babase +import bauiv1lib.mainmenu as bastd_ui_mainmenu +import bauiv1 as bui +import bascenev1 as bs +current_server_ip = "127.0.0.1" +current_server_port = 43210 +servers = [] + + +def _refresh_in_game(func): + def wrapper(self, *args, **kwargs): + returnValue = func(self, *args, **kwargs) + uiscale = bui.app.ui_v1.uiscale + bui.containerwidget( + edit=self._root_widget, + size=(self._width*2, self._height), # double the width + scale=( + 2.15 + if uiscale is bui.UIScale.SMALL + else 1.6 + if uiscale is bui.UIScale.MEDIUM + else 1.0 + ), + ) + h = 125 + v = self._height - 60.0 + bui.textwidget( + parent=self._root_widget, + draw_controller=None, + text="IP: "+current_server_ip+" PORT: "+str(current_server_port), + position=(h-self._button_width/2 + 130, v+60), + h_align='center', + v_align='center', + size=(20, 60), + scale=0.6) + self._public_servers = bui.buttonwidget( + color=(0.8, 0.45, 1), + parent=self._root_widget, + position=(h+self._button_width-10, v+60+20), + size=(self._button_width/4, self._button_height/2), + scale=1.0, + autoselect=self._use_autoselect, + label="~~~", + on_activate_call=bs.Call(public_servers)) + for server in servers: + self._server_button = bui.buttonwidget( + color=(0.8, 0, 1), + parent=self._root_widget, + position=((h - self._button_width / 2) + self._button_width + 20, v), + size=(self._button_width, self._button_height), + scale=1.0, + autoselect=self._use_autoselect, + label=server["name"][0:22], + on_activate_call=bs.Call(bs.connect_to_party, server["ip"], server["port"])) + + v -= 50 + + return returnValue + return wrapper + + +connect = bs.connect_to_party + + +def connect_to_party(address, port=43210, print_progress=False): + global current_server_ip + global current_server_port + if (bs.get_connection_to_host_info() != {}): + bs.disconnect_from_host() + current_server_ip = address + current_server_port = port + connect(address, port, print_progress) + babase.apptimer(1, check_connect_status) + + +def check_connect_status(): + global servers + global current_server_ip + global current_server_port + if (bs.get_connection_to_host_info() != {}): + if (not bs.get_connection_to_host_info()['name']): + babase.apptimer(1, check_connect_status) + return + new_server = {"name": bs.get_connection_to_host_info( + )['name'], "ip": current_server_ip, "port": current_server_port} + if new_server not in servers: + servers.append(new_server) + servers = servers[-3:] + else: + print("connection failed falling back to gather window") + public_servers() + + +def public_servers(origin=None): + from bauiv1lib.gather import GatherWindow + bui.app.ui_v1.set_main_menu_window(GatherWindow(origin_widget=origin).get_root_widget()) + +# ba_meta export plugin + + +class bySmoothy(babase.Plugin): + def __init__(self): + bastd_ui_mainmenu.MainMenuWindow._refresh_in_game = _refresh_in_game( + bastd_ui_mainmenu.MainMenuWindow._refresh_in_game) + bs.connect_to_party = connect_to_party diff --git a/plugins/utilities/share_replay.py b/plugins/utilities/share_replay.py new file mode 100644 index 000000000..3ba673c31 --- /dev/null +++ b/plugins/utilities/share_replay.py @@ -0,0 +1,409 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +""" + Plugin by LoupGarou a.k.a Loup/Soup + Discord →ʟօʊքɢǟʀօʊ#3063 +Share replays easily with your friends or have a backup + +Exported replays are stored in replays folder which is inside mods folder +You can start sharing replays by opening the watch window and going to share replay tab + +Feel free to let me know if you use this plugin,i love to hear that :) + +Message me in discord if you find some bug +Use this code for your experiments or plugin but please dont rename this plugin and distribute with your name,don't do that,its bad' +""" + +# ba_meta require api 9 +from __future__ import annotations +from typing import TYPE_CHECKING, cast +if TYPE_CHECKING: + from typing import Any, Sequence, Callable, List, Dict, Tuple, Optional, Union + +from os import listdir, mkdir, path, sep, remove +from shutil import copy, copytree + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +from enum import Enum +from bauiv1lib.tabs import TabRow +from bauiv1lib.confirm import ConfirmWindow +from bauiv1lib.watch import WatchWindow +from bauiv1lib.popup import PopupWindow + + +title = "SHARE REPLAY" +internal_dir = _babase.get_replays_dir()+sep +external_dir = path.join(_babase.env()["python_directory_user"], "replays"+sep) +uiscale = bui.app.ui_v1.uiscale + +# colors +pink = (1, 0.2, 0.8) +green = (0.4, 1, 0.4) +red = (1, 0, 0) +blue = (0.26, 0.65, 0.94) +blue_highlight = (0.4, 0.7, 1) +b_color = (0.6, 0.53, 0.63) +b_textcolor = (0.75, 0.7, 0.8) + + +def Print(*args, color=None): + out = "" + for arg in args: + a = str(arg) + out += a + bui.screenmessage(out, color=color) + + +def cprint(*args): + out = "" + for arg in args: + a = str(arg) + out += a + bs.chatmessage(out) + + +if not path.exists(external_dir): + mkdir(external_dir) + Print("You are ready to share replays", color=pink) + + +def override(cls: ClassType) -> Callable[[MethodType], MethodType]: + def decorator(newfunc: MethodType) -> MethodType: + funcname = newfunc.__code__.co_name + if hasattr(cls, funcname): + oldfunc = getattr(cls, funcname) + setattr(cls, f'_old_{funcname}', oldfunc) + + setattr(cls, funcname, newfunc) + return newfunc + + return decorator + + +class CommonUtilities: + + def sync_confirmation(self): + ConfirmWindow(text="WARNING:\nreplays with same name in mods folder\n will be overwritten", + action=self.sync, cancel_is_selected=True) + + def sync(self): + internal_list = listdir(internal_dir) + external_list = listdir(external_dir) + for i in internal_list: + copy(internal_dir+sep+i, external_dir+sep+i) + for i in external_list: + if i in internal_list: + pass + else: + copy(external_dir+sep+i, internal_dir+sep+i) + Print("Synced all replays", color=pink) + + def _copy(self, selected_replay, tab_id): + if selected_replay is None: + Print("Select a replay", color=red) + return + elif tab_id == MyTabId.INTERNAL: + copy(internal_dir+selected_replay, external_dir+selected_replay) + Print(selected_replay[0:-4]+" exported", color=pink) + else: + copy(external_dir+selected_replay, internal_dir+selected_replay) + Print(selected_replay[0:-4]+" imported", color=green) + + def delete_replay(self, selected_replay, tab_id, cls_inst): + if selected_replay is None: + Print("Select a replay", color=red) + return + + def do_it(): + if tab_id == MyTabId.INTERNAL: + remove(internal_dir+selected_replay) + elif tab_id == MyTabId.EXTERNAL: + remove(external_dir+selected_replay) + cls_inst.on_tab_select(tab_id) # updating the tab + Print(selected_replay[0:-4]+" was deleted", color=red) + ConfirmWindow(text=f"Delete \"{selected_replay.split('.')[0]}\" \nfrom {'internal directory' if tab_id == MyTabId.INTERNAL else 'external directory'}?", + action=do_it, cancel_is_selected=True) + + +CommonUtils = CommonUtilities() + + +class MyTabId(Enum): + INTERNAL = "internal" + EXTERNAL = "external" + SHARE_REPLAYS = "share_replay" + + +class Help(PopupWindow): + def __init__(self): + self.width = 1200 + self.height = 250 + self.root_widget = bui.Window(bui.containerwidget( + size=(self.width, self.height), on_outside_click_call=self.close, transition="in_right")).get_root_widget() + + bui.containerwidget(edit=self.root_widget, on_outside_click_call=self.close) + bui.textwidget(parent=self.root_widget, position=(0, self.height * 0.7), corner_scale=1.2, color=green, + text=f"»Replays are exported to\n {external_dir}\n»Copy replays to the above folder to be able to import them into the game\n»I would love to hear from you,meet me on discord\n -LoupGarou(author)") + + def close(self): + bui.getsound('swish').play() + bui.containerwidget(edit=self.root_widget, transition="out_right",) + + +class ShareTabUi(WatchWindow): + def __init__(self, root_widget=None): + self.tab_id = MyTabId.INTERNAL + self.selected_replay = None + + if root_widget is None: + self.root = bui.Window(bui.containerwidget( + size=(1000, 600), on_outside_click_call=self.close, transition="in_right")).get_root_widget() + + else: + self.root = root_widget + + self.draw_ui() + + def on_select_text(self, widget, name): + existing_widgets = self.scroll2.get_children() + for i in existing_widgets: + bui.textwidget(edit=i, color=(1, 1, 1)) + bui.textwidget(edit=widget, color=(1.0, 1, 0.4)) + self.selected_replay = name + + def on_tab_select(self, tab_id): + self.selected_replay = None + self.tab_id = tab_id + t_scale = 1.6 + + if tab_id == MyTabId.INTERNAL: + dir_list = listdir(internal_dir) + bui.buttonwidget(edit=self.share_button, label="Export\nReplay") + else: + dir_list = listdir(external_dir) + bui.buttonwidget(edit=self.share_button, label="Import\nReplay") + + self.tab_row.update_appearance(tab_id) + dir_list = sorted(dir_list) + existing_widgets = self.scroll2.get_children() + if existing_widgets: # deleting textwidgets from old tab + for i in existing_widgets: + i.delete() + height = 900 + for i in dir_list: # making textwidgets for all replays + height -= 50 + a = i + i = bui.textwidget( + parent=self.scroll2, + size=(self._my_replays_scroll_width/t_scale, 30), + text=i.split(".")[0], + position=(20, height), + selectable=True, + max_chars=40, + corner_scale=t_scale, + click_activate=True, + always_highlight=True,) + bui.textwidget(edit=i, on_activate_call=babase.Call(self.on_select_text, i, a)) + + def draw_ui(self): + self._r = 'watchWindow' + x_inset = 100 if uiscale is babase.UIScale.SMALL else 0 + scroll_buffer_h = 130 + 2 * x_inset + self._width = 1240 if uiscale is babase.UIScale.SMALL else 1040 + self._height = ( + 578 + if uiscale is babase.UIScale.SMALL + else 670 + if uiscale is babase.UIScale.MEDIUM + else 800) + self._scroll_width = self._width - scroll_buffer_h + self._scroll_height = self._height - 180 + # + c_width = self._scroll_width + c_height = self._scroll_height - 20 + sub_scroll_height = c_height - 63 + self._my_replays_scroll_width = sub_scroll_width = ( + 680 if uiscale is babase.UIScale.SMALL else 640 + ) + + v = c_height - 30 + b_width = 140 if uiscale is babase.UIScale.SMALL else 178 + b_height = ( + 107 + if uiscale is babase.UIScale.SMALL + else 142 + if uiscale is babase.UIScale.MEDIUM + else 190 + ) + b_space_extra = ( + 0 + if uiscale is babase.UIScale.SMALL + else -2 + if uiscale is babase.UIScale.MEDIUM + else -5 + ) + + b_color = (0.6, 0.53, 0.63) + b_textcolor = (0.75, 0.7, 0.8) + btnv = (c_height - (48 + if uiscale is babase.UIScale.SMALL + else 45 + if uiscale is babase.UIScale.MEDIUM + else 40) - b_height) + btnh = 40 if uiscale is babase.UIScale.SMALL else 40 + smlh = 190 if uiscale is babase.UIScale.SMALL else 225 + tscl = 1.0 if uiscale is babase.UIScale.SMALL else 1.2 + + stab_width = 500 + stab_height = 300 + stab_h = smlh + + v -= sub_scroll_height + 23 + scroll = bui.scrollwidget( + parent=self.root, + position=(smlh, v), + size=(sub_scroll_width, sub_scroll_height), + ) + + self.scroll2 = bui.columnwidget(parent=scroll, + size=(sub_scroll_width, sub_scroll_height)) + + tabdefs = [(MyTabId.INTERNAL, 'INTERNAL'), (MyTabId.EXTERNAL, "EXTERNAL")] + self.tab_row = TabRow(self.root, tabdefs, pos=(stab_h, sub_scroll_height), + size=(stab_width, stab_height), on_select_call=self.on_tab_select) + + helpbtn_space = 20 + helpbtn_v = stab_h+stab_width+helpbtn_space+120 + helpbtn_h = sub_scroll_height+helpbtn_space + + bui.buttonwidget( + parent=self.root, + position=(helpbtn_v, helpbtn_h), + size=(35, 35), + button_type="square", + label="?", + text_scale=1.5, + color=b_color, + textcolor=b_textcolor, + on_activate_call=Help) + + def call_copy(): return CommonUtils._copy(self.selected_replay, self.tab_id) + self.share_button = bui.buttonwidget( + parent=self.root, + size=(b_width, b_height), + position=(btnh, btnv), + button_type="square", + label="Export\nReplay", + text_scale=tscl, + color=b_color, + textcolor=b_textcolor, + on_activate_call=call_copy) + + btnv -= b_height + b_space_extra + sync_button = bui.buttonwidget( + parent=self.root, + size=(b_width, b_height), + position=(btnh, btnv), + button_type="square", + label="Sync\nReplay", + text_scale=tscl, + color=b_color, + textcolor=b_textcolor, + on_activate_call=CommonUtils.sync_confirmation) + + btnv -= b_height + b_space_extra + def call_delete(): return CommonUtils.delete_replay(self.selected_replay, self.tab_id, self) + delete_replay_button = bui.buttonwidget( + parent=self.root, + size=(b_width, b_height), + position=(btnh, btnv), + button_type="square", + label=babase.Lstr(resource=self._r + '.deleteReplayButtonText'), + text_scale=tscl, + color=b_color, + textcolor=b_textcolor, + on_activate_call=call_delete) + + self.on_tab_select(MyTabId.INTERNAL) + + def close(self): + bui.getsound('swish').play() + bui.containerwidget(edit=self.root, transition="out_right",) + + +# ++++++++++++++++for keyboard navigation++++++++++++++++ + + # bui.widget(edit=self.enable_button, up_widget=decrease_button, down_widget=self.lower_text,left_widget=save_button, right_widget=save_button) + +# ---------------------------------------------------------------------------------------------------- + +class ShareTab(WatchWindow): + + @override(WatchWindow) + def __init__(self, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + oldmethod=None): + self.my_tab_container = None + self._old___init__(transition, origin_widget) + + self._tab_row.tabs[self.TabID.MY_REPLAYS].button.delete() # deleting old tab button + + tabdefs = [(self.TabID.MY_REPLAYS, + babase.Lstr(resource=self._r + '.myReplaysText'),), + (MyTabId.SHARE_REPLAYS, "Share Replays"),] + + uiscale = bui.app.ui_v1.uiscale + x_inset = 100 if uiscale is babase.UIScale.SMALL else 0 + tab_buffer_h = 750 + 2 * x_inset + self._tab_row = TabRow( + self._root_widget, + tabdefs, + pos=((tab_buffer_h / 1.5) * 0.5, self._height - 130), + size=((self._width - tab_buffer_h)*2, 50), + on_select_call=self._set_tab) + + self._tab_row.update_appearance(self.TabID.MY_REPLAYS) + + @override(WatchWindow) + def _set_tab(self, tab_id, oldfunc=None): + self._old__set_tab(tab_id) + if self.my_tab_container: + self.my_tab_container.delete() + if tab_id == MyTabId.SHARE_REPLAYS: + + scroll_left = (self._width - self._scroll_width) * 0.5 + scroll_bottom = self._height - self._scroll_height - 79 - 48 + + c_width = self._scroll_width + c_height = self._scroll_height - 20 + sub_scroll_height = c_height - 63 + self._my_replays_scroll_width = sub_scroll_width = ( + 680 if uiscale is babase.UIScale.SMALL else 640 + ) + + self.my_tab_container = bui.containerwidget( + parent=self._root_widget, + position=(scroll_left, + scroll_bottom + (self._scroll_height - c_height) * 0.5,), + size=(c_width, c_height), + background=False, + selection_loops_to_parent=True, + ) + + ShareTabUi(self.my_tab_container) + + +# ba_meta export babase.Plugin + +class Loup(babase.Plugin): + def on_app_running(self): + WatchWindow.__init__ = ShareTab.__init__ + + def has_settings_ui(self): + return True + + def show_settings_ui(self, button): + Print("Open share replay tab in replay window to share your replays", color=blue) diff --git a/plugins/utilities/sorry.py b/plugins/utilities/sorry.py new file mode 100644 index 000000000..ed0276fc3 --- /dev/null +++ b/plugins/utilities/sorry.py @@ -0,0 +1,69 @@ +import babase +import bauiv1 as bui +import bauiv1lib.party +import random +import bascenev1 as bs +from bascenev1 import screenmessage as push + +# "%" random from sory +# "$" random from cash +sory = ["Sorry", "Sry", "Sryyy", "Sorryy"] +cash = ["My bad", "My fault", "My mistake", "My apologize"] +lmao = [ + "Oops %", + "% didn't mean to", + "%, that happens", + "$, apologies!", + "Ah I slipped, very %", + "$, didn't mean to.", + "Ah, % about that", + "A- I did that $", + "%, didn't mean to.", + "$, forgive the slip", + "%, didn't mean to mess up", + "Ah % $", + "$, forgive the error", + "%, $ entirely" +] + + +class SorryPW(bauiv1lib.party.PartyWindow): + def __init__(s, *args, **kwargs): + super().__init__(*args, **kwargs) + s._delay = s._a = 50 # 5 seconds + s._btn = bui.buttonwidget( + parent=s._root_widget, + size=(50, 35), + scale=0.7, + label='Sorry', + button_type='square', + position=(s._width - 60, s._height - 83), + on_activate_call=s._apologize + ) + + def _ok(s, a): + if s._btn.exists(): + bui.buttonwidget(edit=s._btn, label=str((s._delay - a) / 10) + if a != s._delay else 'Sorry') + s._a = a + else: + return + + def _apologize(s): + if s._a != s._delay: + push("Too fast!") + return + else: + bs.chatmessage(random.choice(lmao).replace( + '%', random.choice(sory)).replace('$', random.choice(cash))) + for i in range(10, s._delay+1): + bs.apptimer((i-10)/10, bs.Call(s._ok, i)) + +# ba_meta require api 9 + +# ba_meta export babase.Plugin + + +class byBordd(babase.Plugin): + def __init__(s): + bauiv1lib.party.PartyWindow = SorryPW diff --git a/plugins/utilities/store_event_specials.py b/plugins/utilities/store_event_specials.py index d9c4bdce1..bca82512b 100644 --- a/plugins/utilities/store_event_specials.py +++ b/plugins/utilities/store_event_specials.py @@ -21,15 +21,15 @@ For more information, please refer to """ -# ba_meta require api 7 +# ba_meta require api 9 from typing import List, Dict, Any -import ba -import ba._store -import ba.internal - -original_get_store_layout = ba._store.get_store_layout +import babase +import bauiv1 as bui +from bauiv1lib.store.browser import StoreBrowserWindow +from bauiv1._appsubsystem import UIV1AppSubsystem +original_get_store_layout = bui.app.classic.store.get_store_layout def add_special_characters(layout: @@ -61,7 +61,27 @@ def modified_get_store_layout() -> Dict[str, List[Dict[str, Any]]]: return layout -# ba_meta export plugin -class Main(ba.Plugin): +# ba_meta export babase.Plugin +class Main(babase.Plugin): def on_app_running(self) -> None: - ba.internal.get_store_layout = modified_get_store_layout + bui.app.classic.store.get_store_layout = modified_get_store_layout + + def has_settings_ui(self): + return True + + def show_settings_ui(self, button): + try: + main_window = UIV1AppSubsystem().get_main_window() + if main_window: + main_window.main_window_replace(lambda: StoreBrowserWindow( + show_tab=StoreBrowserWindow.TabID.MINIGAMES, + origin_widget=button, + ) + ) + else: + StoreBrowserWindow( + show_tab=StoreBrowserWindow.TabID.MINIGAMES, + origin_widget=button, + ) + except Exception as e: + print(e) diff --git a/plugins/utilities/tag.py b/plugins/utilities/tag.py new file mode 100644 index 000000000..bc6c9448e --- /dev/null +++ b/plugins/utilities/tag.py @@ -0,0 +1,568 @@ +""" +I apreciate any kind of modification. So feel free to use or edit code or change credit string.... no problem. + +really awsome servers: + Bombsquad Consultancy Service - https://discord.gg/2RKd9QQdQY + bombspot - https://discord.gg/ucyaesh + cyclones - https://discord.gg/pJXxkbQ7kH + +how to use: + Account -> PlayerProfile -> Edit(new profile -> edit) + Open profile you like (every profile has dirrent tags, settings (Configs)) + enable tag for profile you like, edit tag you want. enable cool flashy animation +""" + +from __future__ import annotations +from bauiv1lib.profile.edit import EditProfileWindow +from bauiv1lib.popup import PopupMenu +from bascenev1lib.actor.playerspaz import PlayerSpaz +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase + +from typing import ( + Tuple, + Optional, + Sequence, + Union, + Callable, + Any, + List, + cast +) + +__version__ = 2.0 +__author__ = "pranav1711#2006" + + +# Default Confings/Settings +Configs = { + "enabletag": False, + "tag": "", + "scale": "medium", + "opacity": 1.0, + "shadow": 0.0, + "animtag": False, + "frequency": 0.5 +} + +# Useful global fucntions + + +def setconfigs() -> None: + """ + Set required defualt configs for mod + """ + cnfg = babase.app.config + profiles = cnfg['Player Profiles'] + if not "TagConf" in cnfg: + cnfg["TagConf"] = {} + for p in profiles: + if not p in cnfg["TagConf"]: + cnfg["TagConf"][str(p)] = Configs + babase.app.config.apply_and_commit() + + +def getanimcolor(name: str) -> dict: + """ + Returns dictnary of colors with prefective time -> {seconds: (r, g, b)} + """ + freq = babase.app.config['TagConf'][str(name)]['frequency'] + s1 = 0.0 + s2 = s1 + freq + s3 = s2 + freq + + animcolor = { + s1: (1, 0, 0), + s2: (0, 1, 0), + s3: (0, 0, 1) + } + return animcolor + + +def gethostname() -> str: + """ + Return player name, by using -1 only host can use tags. + """ + session = bs.get_foreground_host_session() + with session.context: + for player in session.sessionplayers: + if player.inputdevice.client_id == -1: + name = player.getname(full=True, icon=False) + break + if name == bui.app.plus.get_v1_account_name(): + return '__account__' + return name + + +# Dummy functions for extend functionality for class object +PlayerSpaz.init = PlayerSpaz.__init__ +EditProfileWindow.init = EditProfileWindow.__init__ + +# PlayerSpaz object at -> bascenev1lib.actor.playerspaz + + +def NewPlayerSzapInit(self, + player: bs.Player, + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + character: str = 'Spaz', + powerups_expire: bool = True) -> None: + self.init(player, + color=color, + highlight=highlight, + character=character, + powerups_expire=powerups_expire) + self.curname = gethostname() + + try: + cnfg = babase.app.config["TagConf"] + if cnfg[str(self.curname)]["enabletag"]: + # Tag node + self.mnode = bs.newnode('math', owner=self.node, attrs={ + 'input1': (0, 1.5, 0), 'operation': 'add'}) + self.node.connectattr('torso_position', self.mnode, 'input2') + + tagtext = cnfg[str(self.curname)]["tag"] + opacity = cnfg[str(self.curname)]["opacity"] + shadow = cnfg[str(self.curname)]["shadow"] + sl = cnfg[str(self.curname)]["scale"] + scale = 0.01 if sl == 'mediam' else 0.009 if not sl == 'large' else 0.02 + + self.Tag = bs.newnode( + type='text', + owner=self.node, + attrs={ + 'text': str(tagtext), + 'in_world': True, + 'shadow': shadow, + 'color': (0, 0, 0), + 'scale': scale, + 'opacity': opacity, + 'flatness': 1.0, + 'h_align': 'center'}) + self.mnode.connectattr('output', self.Tag, 'position') + + if cnfg[str(self.curname)]["animtag"]: + kys = getanimcolor(self.curname) + bs.animate_array(node=self.Tag, attr='color', size=3, keys=kys, loop=True) + except Exception: + pass + + +def NewEditProfileWindowInit(self, + existing_profile: Optional[str], + transition: str = 'in_right', + origin_widget: bui.Widget | None = None) -> None: + """ + New boilerplate for editprofilewindow, addeds button to call TagSettings window + """ + self.existing_profile = existing_profile + self.init(existing_profile, transition, origin_widget) + + v = self._height - 115.0 + x_inset = self._x_inset + b_width = 50 + b_height = 30 + + self.tagwinbtn = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(505 + x_inset, v - 38 - 15), + size=(b_width, b_height), + color=(0.6, 0.5, 0.6), + label='Tag', + button_type='square', + text_scale=1.2, + on_activate_call=babase.Call(_on_tagwinbtn_press, self)) + + +def _on_tagwinbtn_press(self): + """ + Calls tag config window passes all paramisters + """ + if not self.main_window_has_control(): + return + + self.main_window_replace( + TagWindow( + self.existing_profile, + self._name, + transition='in_right', + origin_widget=self.tagwinbtn + ) + ) + +# ba_meta require api 9 + +# ba_meta export babase.Plugin + + +class Tag(babase.Plugin): + def __init__(self) -> None: + """ + Tag above actor player head, replacing PlayerSpaz class for getting actor, + using EditProfileWindow for UI. + """ + if _babase.env().get("build_number", 0) >= 20327: + setconfigs() + self.Replace() + + def Replace(self) -> None: + """ + Replacing bolierplates no harm to relative funtionality only extending + """ + PlayerSpaz.__init__ = NewPlayerSzapInit + EditProfileWindow.__init__ = NewEditProfileWindowInit + + +class TagWindow(bui.MainWindow): + + def __init__(self, + existing_profile: Optional[str], + profilename: str, + transition: Optional[str] = 'in_right', + origin_widget: bui.Widget | None = None): + self.existing_profile = existing_profile + self.profilename = profilename + + uiscale = bui.app.ui_v1.uiscale + self._width = 870.0 if uiscale is babase.UIScale.SMALL else 670.0 + self._height = (390.0 if uiscale is babase.UIScale.SMALL else + 450.0 if uiscale is babase.UIScale.MEDIUM else 520.0) + extra_x = 100 if uiscale is babase.UIScale.SMALL else 0 + self.extra_x = extra_x + top_extra = 20 if uiscale is babase.UIScale.SMALL else 0 + + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height), + scale=(2.06 if uiscale is babase.UIScale.SMALL else + 1.4 if uiscale is babase.UIScale.MEDIUM else 1.0) + ), + transition=transition, + origin_widget=origin_widget) + + self._back_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + selectable=False, # FIXME: when press a in text field it selets to button + position=(52 + self.extra_x, self._height - 60), + size=(60, 60), + scale=0.8, + label=babase.charstr(babase.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self.main_window_back) + bui.containerwidget(edit=self._root_widget, cancel_button=self._back_button) + + self._save_button = bui.buttonwidget( + parent=self._root_widget, + position=(self._width - (177 + extra_x), + self._height - 60), + size=(155, 60), + color=(0, 0.7, 0.5), + autoselect=True, + selectable=False, # FIXME: when press a in text field it selets to button + scale=0.8, + label=babase.Lstr(resource='saveText'), + on_activate_call=self.on_save) + bui.widget(edit=self._save_button, left_widget=self._back_button) + bui.widget(edit=self._back_button, right_widget=self._save_button) + bui.containerwidget(edit=self._root_widget, start_button=self._save_button) + + self._title_text = bui.textwidget( + parent=self._root_widget, + position=(0, self._height - 52 - top_extra), + size=(self._width, 25), + text='Tag', + color=bui.app.ui_v1.title_color, + scale=1.5, + h_align='center', + v_align='top') + + self._scroll_width = self._width - (100 + 2 * extra_x) + self._scroll_height = self._height - 115.0 + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 724.0 + self._spacing = 32 + self._extra_button_spacing = self._spacing * 2.5 + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=(50 + extra_x, 50), + simple_culling_v=20.0, + highlight=False, + size=(self._scroll_width, + self._scroll_height), + selection_loops_to_parent=True) + bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) + + self._subcontainer = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False, + selection_loops_to_parent=True) + + v = self._sub_height - 35 + v -= self._spacing * 1.2 + + self._prof = babase.app.config["TagConf"][self.profilename] + self.enabletagcb = bui.checkboxwidget( + parent=self._subcontainer, + autoselect=False, + position=(10.0, v + 30), + size=(10, 10), + text='Enable Tag', + textcolor=(0.8, 0.8, 0.8), + value=self._prof['enabletag'], + on_value_change_call=babase.Call(self.change_val, [f'{self.profilename}', 'enabletag']), + scale=1.1 if uiscale is babase.UIScale.SMALL else 1.5, + maxwidth=430) + + self.tag_text = bui.textwidget( + parent=self._subcontainer, + text='Tag', + position=(25.0, v - 30), + flatness=1.0, + scale=1.55, + maxwidth=430, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8)) + + self.tagtextfield = bui.textwidget( + parent=self._subcontainer, + position=(100.0, v - 45), + size=(350, 50), + text=self._prof["tag"], + h_align='center', + v_align='center', + max_chars=16, + autoselect=True, + editable=True, + padding=4, + color=(0.9, 0.9, 0.9, 1.0)) + + self.tag_color_text = bui.textwidget( + parent=self._subcontainer, + text='Color', + position=(40.0, v - 80), + flatness=1.0, + scale=1.25, + maxwidth=430, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8)) + + self.tag_scale_text = bui.textwidget( + parent=self._subcontainer, + text='Scale', + position=(40.0, v - 130), + flatness=1.0, + scale=1.25, + maxwidth=430, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8)) + + self.tag_scale_button = PopupMenu( + parent=self._subcontainer, + position=(330.0, v - 145), + width=150, + autoselect=True, + on_value_change_call=bs.WeakCall(self._on_menu_choice), + choices=['large', 'medium', 'small'], + button_size=(150, 50), + # choices_display=('large', 'medium', 'small'), + current_choice=self._prof["scale"]) + + CustomConfigNumberEdit( + parent=self._subcontainer, + position=(40.0, v - 180), + xoffset=65, + displayname='Opacity', + configkey=['TagConf', f'{self.profilename}', 'opacity'], + changesound=False, + minval=0.5, + maxval=2.0, + increment=0.1, + textscale=1.25) + + CustomConfigNumberEdit( + parent=self._subcontainer, + position=(40.0, v - 230), + xoffset=65, + displayname='Shadow', + configkey=['TagConf', f'{self.profilename}', 'shadow'], + changesound=False, + minval=0.0, + maxval=2.0, + increment=0.1, + textscale=1.25) + + self.enabletaganim = bui.checkboxwidget( + parent=self._subcontainer, + autoselect=True, + position=(10.0, v - 280), + size=(10, 10), + text='Animate tag', + textcolor=(0.8, 0.8, 0.8), + value=self._prof['enabletag'], + on_value_change_call=babase.Call(self.change_val, [f'{self.profilename}', 'animtag']), + scale=1.1 if uiscale is babase.UIScale.SMALL else 1.5, + maxwidth=430) + + CustomConfigNumberEdit( + parent=self._subcontainer, + position=(40.0, v - 330), + xoffset=65, + displayname='Frequency', + configkey=['TagConf', f'{self.profilename}', 'frequency'], + changesound=False, + minval=0.1, + maxval=5.0, + increment=0.1, + textscale=1.25) + + def change_val(self, config: List[str], val: bool) -> None: + """ + chamges the value of check boxes + """ + cnfg = babase.app.config["TagConf"] + try: + cnfg[config[0]][config[1]] = val + bui.getsound('gunCocking').play() + except Exception: + bui.screenmessage("error", color=(1, 0, 0)) + bui.getsound('error').play() + babase.app.config.apply_and_commit() + + def _on_menu_choice(self, choice: str): + """ + Changes the given choice in configs + """ + cnfg = babase.app.config["TagConf"][self.profilename] + cnfg["scale"] = choice + babase.app.config.apply_and_commit() + + def on_save(self): + """ + Gets the text in text field of tag and then save it + """ + text: str = cast(str, bui.textwidget(query=self.tagtextfield)) + profile = babase.app.config["TagConf"][self.profilename] + if not text == "" or not text.strip(): + profile['tag'] = text + babase.app.config.apply_and_commit() + bui.getsound('gunCocking').play() + else: + bui.screenmessage(f"please define tag", color=(1, 0, 0)) + bui.getsound('error').play() + + self.main_window_back() + + +class CustomConfigNumberEdit: + """A set of controls for editing a numeric config value. + + It will automatically save and apply the config when its + value changes. + + Attributes: + + nametext + The text widget displaying the name. + + valuetext + The text widget displaying the current value. + + minusbutton + The button widget used to reduce the value. + + plusbutton + The button widget used to increase the value. + """ + + def __init__(self, + parent: bui.Widget, + configkey: List[str], + position: Tuple[float, float], + minval: float = 0.0, + maxval: float = 100.0, + increment: float = 1.0, + callback: Callable[[float], Any] = None, + xoffset: float = 0.0, + displayname: Union[str, babase.Lstr] = None, + changesound: bool = True, + textscale: float = 1.0): + self._minval = minval + self._maxval = maxval + self._increment = increment + self._callback = callback + self._configkey = configkey + self._value = babase.app.config[configkey[0]][configkey[1]][configkey[2]] + + self.nametext = bui.textwidget( + parent=parent, + position=position, + size=(100, 30), + text=displayname, + maxwidth=160 + xoffset, + color=(0.8, 0.8, 0.8, 1.0), + h_align='left', + v_align='center', + scale=textscale) + + self.valuetext = bui.textwidget( + parent=parent, + position=(246 + xoffset, position[1]), + size=(60, 28), + editable=False, + color=(0.3, 1.0, 0.3, 1.0), + h_align='right', + v_align='center', + text=str(self._value), + padding=2) + + self.minusbutton = bui.buttonwidget( + parent=parent, + position=(330 + xoffset, position[1]), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=babase.Call(self._down), + repeat=True, + enable_sound=changesound) + + self.plusbutton = bui.buttonwidget(parent=parent, + position=(380 + xoffset, position[1]), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=babase.Call(self._up), + repeat=True, + enable_sound=changesound) + + bui.uicleanupcheck(self, self.nametext) + self._update_display() + + def _up(self) -> None: + self._value = min(self._maxval, self._value + self._increment) + self._changed() + + def _down(self) -> None: + self._value = max(self._minval, self._value - self._increment) + self._changed() + + def _changed(self) -> None: + self._update_display() + if self._callback: + self._callback(self._value) + babase.app.config[self._configkey[0]][self._configkey[1] + ][self._configkey[2]] = float(str(f'{self._value:.1f}')) + babase.app.config.apply_and_commit() + + def _update_display(self) -> None: + bui.textwidget(edit=self.valuetext, text=f'{self._value:.1f}') diff --git a/plugins/utilities/tnt_respawn_text.py b/plugins/utilities/tnt_respawn_text.py new file mode 100644 index 000000000..669729601 --- /dev/null +++ b/plugins/utilities/tnt_respawn_text.py @@ -0,0 +1,220 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 9 + +""" + TNT Respawn Text by TheMikirog + Version 1 + + Shows when a TNT box is about to respawn with non-intrusive text. + + Heavily commented for easy modding learning! + + No Rights Reserved +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +# Let's import everything we need and nothing more. +import babase +import bauiv1 as bui +import bascenev1 as bs +import bascenev1lib +import math +import random +from bascenev1lib.actor.bomb import Bomb + +if TYPE_CHECKING: + pass + +""" + Turns out TNT respawning got changed during the 1.5 update. + At first I planned to make an accurate timer text that just counts down seconds to respawn. + However, to prevent timer stacking, Eric decided to update the timer every 1.1s seconds instead of the standard 1.0s. + This makes it a pain to mod, so instead I had to make some compromises and go for a percentage charge instead. + + The goal here is to make it easier to intuit when the box respawns, so you can still play around it. + Percentage until respawn is still more helpful than absolutely nothing. + I wanted to keep the original TNT box's respawn design here, so I didn't touch the timer. + I prefer adding onto existing behavior than editing existing code. + This mod is supposed to be a quality of life thing after all. +""" + + +# ba_meta export babase.Plugin +class TNTRespawnText(babase.Plugin): + + # This clamping function will make sure a certain value can't go above or below a certain threshold. + # We're gonna need this functionality in just a bit. + def clamp(num, min_value, max_value): + num = max(min(num, max_value), min_value) + return num + + # This function gets called every time the TNT dies. Doesn't matter how. + # Explosions, getting thrown out of bounds, stuff. + # I want the text appearing animation to start as soon as the TNT box blows up. + def on_tnt_exploded(self): + self.tnt_has_callback = False + self._respawn_text.color = (1.0, 1.0, 1.0) + bs.animate( + self._respawn_text, + 'opacity', + { + 0: 0.0, + self._respawn_time * 0.5: 0.175, + self._respawn_time: 0.4 + }, + ) + + # We're gonna use the magic of decorators to expand the original code with new stuff. + # This even works with other mods too! Don't replace functions, use decorators! + # Anyway we're gonna access the TNTSpawner class' init function. + def new_init(func): + def wrapper(*args, **kwargs): + + # The update function is not only called by a timer, but also manually + # during the original init function's execution. + # This means the code expects a variable that doesn't exist. + # Let's make it prematurely. + # args[0] is "self" in the original game code. + args[0]._respawn_text = None + + # This is where the original game's code is executed. + func(*args, **kwargs) + + # For each TNT we make we want to add a callback. + # It's basically a flag that tells the TNT to call a function. + # We don't want to add several of the same flag at once. + # We set this to True every time we add a callback. + # We check for this variable before adding a new one. + args[0].tnt_has_callback = True + + # Let's make the text. + # We tap into the spawner position in order to decide where the text should be. + respawn_text_position = (args[0]._position[0], + args[0]._position[1] - 0.4, + args[0]._position[2]) + args[0]._respawn_text = bs.newnode( + 'text', + attrs={ + 'text': "", # we'll set the text later + 'in_world': True, + 'position': respawn_text_position, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (1.0, 1.0, 1.0), + 'opacity': 0.0, + 'scale': 0.0225, + 'h_align': 'center', + 'v_align': 'center', + }, + ) + # Here we add our callback. + # Timers don't like calling functions that are outside of the game's "universe". + # If we call the function directly, we get a PyCallable error. + # We make a dummy function to avoid this. + + def tnt_callback(): + TNTRespawnText.on_tnt_exploded(args[0]) + + # One disadvantage of the documentation is that it doesn't tell you all functions related to the node system. + # To learn about all possible atttributes and functions you just gotta explore the code and experiment. + # This add_death_action function of the node system is used in the original game + # to let the player know if the node got removed. + # For bombs that would be explosions or when they go out of bounds. + # This is used to increase the owner's bomb count by one. + # Here however we'll use this function to manipulate our text logic. + # We want to animate our text the moment the TNT box dies. + args[0]._tnt.node.add_death_action(tnt_callback) + return wrapper + # Let's replace the original init function with our modified version. + bascenev1lib.actor.bomb.TNTSpawner.__init__ = new_init( + bascenev1lib.actor.bomb.TNTSpawner.__init__) + + # Our modified update function. + # This gets called every 1.1s. Check the TNTSpawner class in the game's code for details. + def new_update(func): + def wrapper(*args, **kwargs): + + # Check if our TNT box is still kickin'. + tnt_alive = args[0]._tnt is not None and args[0]._tnt.node + + func(*args, **kwargs) # original code + + # The first time this code executes, nothing happens. + # However once our text node is created properly, let's do some work. + if args[0]._respawn_text: + + # Let's make a value that will represent percentage. + # 0 means timer started and 100 means ready. + value = args[0]._wait_time / args[0]._respawn_time + + # It's annoying when the number jumps from 99% to 100% and it's delayed. + # Let's make sure this happens less often. + # I turned a linear curve into an exponential one. + value = math.pow(value - 0.001, 2) + + # Let's turn the value into a percentage. + value = math.floor(value * 100) + + # Let's make sure it's actually between 0 and 100. + value = TNTRespawnText.clamp(value, 0, 100) + + # Let's finish it off with a percentage symbol and preso! + args[0]._respawn_text.text = str(value)+"%" + + # When the timer ticks, we do different things depending on the time and the state of our TNT box. + if not tnt_alive: + # Code goes here if we don't have a TNT box and we reached 100%. + if args[0]._tnt is None or args[0]._wait_time >= args[0]._respawn_time and args[0]._respawn_text: + # Animate the text "bounce" to draw attention + bs.animate( + args[0]._respawn_text, + 'scale', + { + 0: args[0]._respawn_text.scale * 1.2, + 0.3: args[0]._respawn_text.scale * 1.05, + 0.6: args[0]._respawn_text.scale * 1.025, + 1.1: args[0]._respawn_text.scale + }, + ) + # Fade the text away + bs.animate( + args[0]._respawn_text, + 'opacity', + { + 0: args[0]._respawn_text.opacity, + 1.1: 0.0 + }, + ) + # Make sure it says 100%, because our value we calculated earlier might not be accurate at that point. + args[0]._respawn_text.text = "100%" + + # Make our text orange. + args[0]._respawn_text.color = (1.0, 0.75, 0.5) + + # Make some sparks to draw the eye. + bs.emitfx( + position=args[0]._position, + count=int(5.0 + random.random() * 10), + scale=0.8, + spread=1.25, + chunk_type='spark', + ) + # What if we still have our TNT box? + else: + # If the TNT box is fresly spawned spawned earlier in the function, chances are it doesn't have a callback. + # If it has, ignore. Otherwise let's add it. + # Cloning code that already exists in init is not very clean, but that'll do. + if args[0].tnt_has_callback: + return + + def tnt_callback(): + TNTRespawnText.on_tnt_exploded(args[0]) + args[0]._tnt.node.add_death_action(tnt_callback) + return wrapper + + # Let's replace the original update function with our modified version. + bascenev1lib.actor.bomb.TNTSpawner._update = new_update( + bascenev1lib.actor.bomb.TNTSpawner._update) diff --git a/plugins/utilities/topmsg.py b/plugins/utilities/topmsg.py new file mode 100644 index 000000000..13cdea3c9 --- /dev/null +++ b/plugins/utilities/topmsg.py @@ -0,0 +1,33 @@ +# Copyright 2025 - Solely by BrotherBoard +# Intended for personal use only +# Bug? Feedback? Telegram >> @BroBordd + +""" +TopMsg v1.1.2 - Chat top right + +When chat is muted, shows chat messages top right. +Prevents spam and flooding screen. +Does not repeat messages. +""" + +from babase import app, Plugin +from bascenev1 import ( + get_chat_messages as gcm, + broadcastmessage as push, + apptimer as z +) + +# ba_meta require api 9 +# ba_meta export babase.Plugin + + +class byBordd(Plugin): + def __init__(s): return (setattr(s, 'la', None), z(5, s.ear))[1] + + def ear(s): + a = gcm() + if a and s.la != a[-1]: + if app.config.resolve('Chat Muted'): + push(a[-1], (1, 1, 1), True) + s.la = a[-1] + z(0.1, s.ear) diff --git a/plugins/utilities/translate.py b/plugins/utilities/translate.py new file mode 100644 index 000000000..241f7942a --- /dev/null +++ b/plugins/utilities/translate.py @@ -0,0 +1,259 @@ +# Made by your friend: Freaku + +# Translate function through google webpage by: OnurV2 (from their BombsquadDetails.py mod) +# Github: https://github.com/OnurV2 +# YT: https://m.youtube.com/@OnurV2 + + +import babase +import bauiv1 as bui +from bauiv1lib.popup import PopupMenu +import bauiv1lib.party +import urllib +import threading +import random + +show_translate_result = True +config = babase.app.config +default_config = {'O Source Trans Lang': 'Auto Detect', 'O Target Trans Lang': babase.app.locale.default_locale, + 'Y Source Trans Lang': 'Auto Detect', 'Y Target Trans Lang': babase.app.locale.default_locale} + +for key in default_config: + if not key in config: + config[key] = default_config[key] + +translate_languages = {'Auto Detect': 'auto', 'Arabic': 'ar', 'Chinese (simplified)': 'zh-CN', 'Chinese (traditional)': 'zh-TW', 'Croatian': 'hr', 'Czech': 'cs', + 'Danish': 'da', 'Dutch': 'nl', 'English': 'en', 'Esperanto': 'eo', + 'Finnish': 'fi', + 'Tagalog': 'tl', 'French': 'fr', 'German': 'de', 'Greek': 'el', + 'Hindi': 'hi', 'Hungarian': 'hu', 'Indonesian': 'id', 'Italian': 'it', + 'Japanese': 'ja', + 'Korean': 'ko', 'Malay': 'ms', 'Malayalam': 'ml', 'Marathi': 'mr', 'Persian': 'fa', 'Polish': 'pl', + 'Portuguese': 'pt', 'Romanian': 'ro', 'Russian': 'ru', 'Serbian': 'sr', + 'Slovak': 'sk', 'Spanish': 'es', 'Swedish': 'sv', 'Tamil': 'ta', + 'Telugu': 'te', + 'Thai': 'th', 'Turkish': 'tr', 'Ukrainian': 'uk', 'Vietnamese': 'vi'} +available_translate_languages = [] +for lang in translate_languages: + available_translate_languages.append(lang) +available_translate_languages.sort() +available_translate_languages.remove('Auto Detect') +available_translate_languages.insert(0, 'Auto Detect') + + +def translate(text, _callback, source='auto', target='en'): + text = urllib.parse.quote(text) + url = f'https://translate.google.com/m?tl={target}&sl={source}&q={text}' + request = urllib.request.Request(url) + data = urllib.request.urlopen(request).read().decode('utf-8') + result = data[(data.find('"result-container">'))+len('"result-container">') + :data.find('