From f0b36c742be273dc97083d90eff31088d4dc22f8 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:05:30 +0530 Subject: [PATCH 01/84] fix_moreWindow_scale --- CHANGELOG.md | 4 ++++ index.json | 1 + plugin_manager.py | 5 +++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65c4e00..031ef8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Plugin Manager (dd-mm-yyyy) +### 1.1.6 (26-01-2026) + +- Fix UI scaling for more window + ### 1.1.5 (26-01-2026) - Various UI bugfixes and improvements diff --git a/index.json b/index.json index e082962..01eb39a 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,7 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { + "1.1.6": null, "1.1.5": { "api_version": 9, "commit_sha": "dd1a605", diff --git a/plugin_manager.py b/plugin_manager.py index 0b76453..b5fa069 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -64,9 +64,10 @@ def _regexp_friendly_class_name_shortcut(string): return string.replace(".", "\\ def _by_scale(a, b, c): + u = _uiscale() return ( - a if _uiscale is babase.UIScale.SMALL else - b if _uiscale is babase.UIScale.MEDIUM else + a if u is babase.UIScale.SMALL else + b if u is babase.UIScale.MEDIUM else c ) From 81978848648def2131dd9eee7ccc6a24bdfc3003 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:36:37 +0000 Subject: [PATCH 02/84] [ci] apply-version-metadata --- index.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.json b/index.json index 01eb39a..2984b0a 100644 --- a/index.json +++ b/index.json @@ -1,7 +1,12 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { - "1.1.6": null, + "1.1.6": { + "api_version": 9, + "commit_sha": "f0b36c7", + "released_on": "26-01-2026", + "md5sum": "157026aa0c09b762aff92c73048bafcd" + }, "1.1.5": { "api_version": 9, "commit_sha": "dd1a605", From b0c463e32cdebaebc6800460794f4dd4925e5ce5 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:10:31 +0530 Subject: [PATCH 03/84] ci --- index.json | 7 +------ plugin_manager.py | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/index.json b/index.json index 2984b0a..01eb39a 100644 --- a/index.json +++ b/index.json @@ -1,12 +1,7 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { - "1.1.6": { - "api_version": 9, - "commit_sha": "f0b36c7", - "released_on": "26-01-2026", - "md5sum": "157026aa0c09b762aff92c73048bafcd" - }, + "1.1.6": null, "1.1.5": { "api_version": 9, "commit_sha": "dd1a605", diff --git a/plugin_manager.py b/plugin_manager.py index b5fa069..8bb3006 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -26,7 +26,7 @@ from datetime import datetime # Modules used for overriding AllSettingsWindow import logging -PLUGIN_MANAGER_VERSION = "1.1.5" +PLUGIN_MANAGER_VERSION = "1.1.6" 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. From d516c5e2d4bfaa0ef40543e3201f4321a4ae0f17 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:40:59 +0000 Subject: [PATCH 04/84] [ci] apply-version-metadata --- index.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.json b/index.json index 01eb39a..2af4f5f 100644 --- a/index.json +++ b/index.json @@ -1,7 +1,12 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { - "1.1.6": null, + "1.1.6": { + "api_version": 9, + "commit_sha": "b0c463e", + "released_on": "26-01-2026", + "md5sum": "fc42fef8804cbfece0280ecb9491983c" + }, "1.1.5": { "api_version": 9, "commit_sha": "dd1a605", From e97c8dec374e45201e4370017c3a1cde3a8ae1f9 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Sat, 7 Feb 2026 05:18:21 +0530 Subject: [PATCH 05/84] remove global screenmessage in custom_hits plugin --- plugins/utilities/custom_hits.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/utilities/custom_hits.py b/plugins/utilities/custom_hits.py index 09ae8c7..afa7730 100644 --- a/plugins/utilities/custom_hits.py +++ b/plugins/utilities/custom_hits.py @@ -19,7 +19,7 @@ plugman = dict( authors=[ {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, ], - version="1.0.0", + version="1.0.1", ) #################################### @@ -29,7 +29,6 @@ second_damage = (u'\ue048GOOD!\ue048', (0, 1, 1), '2') third_damage = (u'\ue041NICE!\ue041', (0.8, 0.4, 1), '3') fourth_damage = (u'\ue049UPPS\ue049', (1, 1, 0), '4') five_damage = (u'\ue043DEATH!\ue043', (1, 0, 0), '5') -bs.broadcastmessage(u'\ue043 || \ue062WELCOME ATD\ue062 || \ue043', color=(0, 1, 0)) def custom_effects(pos: float, effect: str = None) -> None: From b48a4bda31cabf3e9afd6e0c363ed70cf9c7e865 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:49:15 +0000 Subject: [PATCH 06/84] [ci] apply-plugin-metadata-and-formatting --- plugins/utilities.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/utilities.json b/plugins/utilities.json index 589881d..8c77444 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2394,6 +2394,7 @@ } ], "versions": { + "1.0.1": null, "1.0.0": { "api_version": 9, "commit_sha": "603aa14", From fdfe0c284a03f9343e9a49bae0d639914e3063fe Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:49:16 +0000 Subject: [PATCH 07/84] [ci] apply-version-metadata --- plugins/utilities.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 8c77444..2d97a60 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2394,7 +2394,12 @@ } ], "versions": { - "1.0.1": null, + "1.0.1": { + "api_version": 9, + "commit_sha": "b48a4bd", + "released_on": "06-02-2026", + "md5sum": "6ca4e2288f78e1800c1c6716477d8dd9" + }, "1.0.0": { "api_version": 9, "commit_sha": "603aa14", From 1ff3e6f79052eb36e8b7c857d5ec3f339cc42791 Mon Sep 17 00:00:00 2001 From: DrapBS Date: Sat, 7 Feb 2026 13:20:52 +0100 Subject: [PATCH 08/84] Add files via upload --- plugins/utilities/players_list.py | 59 +++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 plugins/utilities/players_list.py diff --git a/plugins/utilities/players_list.py b/plugins/utilities/players_list.py new file mode 100644 index 0000000..3b3f1ab --- /dev/null +++ b/plugins/utilities/players_list.py @@ -0,0 +1,59 @@ +#======================== works only if you are host or plugin works on the server created On '2026/02/06' +#======================== + +# ba_meta require api 9 +import bascenev1 as bs +import babase + +# ba_meta export babase.Plugin +class ByDrap(bs.Plugin): + def __init__(self): + self.container = None + self.timer = None + + def on_app_running(self): + self.timer = bs.AppTimer(1.0, bs.WeakCall(self._update_list), repeat=True) + + def has_settings_ui(self): + return True + + def show_settings_ui(self, source_widget): + babase.screenmessage("You tapped my settings!") + + def _update_list(self): + activity = bs.get_foreground_host_activity() + + if activity is None: + if self.container: + self.container.delete() + self.container = None + return + + player_names = [] + for p in activity.players: + name = p.getname(full=True) + player_names.append(f" • {name}") + + header = "┏━━━━ Players  ━━━━┓\n\n" + footer = "\n\n┗━━━━━━━━━━━━━━━┛" + players_str = "\n".join(player_names) if player_names else " Wait for players..." + final_text = header + players_str + footer + + with activity.context: + if self.container is None or not self.container.exists(): + self.container = bs.newnode('text', + attrs={ + 'text': final_text, + 'scale': 0.7, + 'color': (0.5, 1.0, 0.5), + 'h_attach': 'right', + 'v_attach': 'top', + 'h_align': 'left', + 'v_align': 'top', + 'position': (-220, -100), + 'flatness': 0.0, + 'shadow': 1.0, + 'res_scale': 1.5 + }) + else: + self.container.text = final_text From 658677f05787d1e493d1625ca9389fd55aed80d5 Mon Sep 17 00:00:00 2001 From: DrapBS <260050423+DrapBS@users.noreply.github.com> Date: Sat, 7 Feb 2026 16:26:10 +0000 Subject: [PATCH 09/84] [ci] apply-plugin-metadata-and-formatting --- plugins/utilities/players_list.py | 36 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/plugins/utilities/players_list.py b/plugins/utilities/players_list.py index 3b3f1ab..b97906b 100644 --- a/plugins/utilities/players_list.py +++ b/plugins/utilities/players_list.py @@ -1,11 +1,13 @@ -#======================== works only if you are host or plugin works on the server created On '2026/02/06' -#======================== +# ======================== works only if you are host or plugin works on the server created On '2026/02/06' +# ======================== # ba_meta require api 9 import bascenev1 as bs import babase # ba_meta export babase.Plugin + + class ByDrap(bs.Plugin): def __init__(self): self.container = None @@ -22,7 +24,7 @@ class ByDrap(bs.Plugin): def _update_list(self): activity = bs.get_foreground_host_activity() - + if activity is None: if self.container: self.container.delete() @@ -33,7 +35,7 @@ class ByDrap(bs.Plugin): for p in activity.players: name = p.getname(full=True) player_names.append(f" • {name}") - + header = "┏━━━━ Players  ━━━━┓\n\n" footer = "\n\n┗━━━━━━━━━━━━━━━┛" players_str = "\n".join(player_names) if player_names else " Wait for players..." @@ -42,18 +44,18 @@ class ByDrap(bs.Plugin): with activity.context: if self.container is None or not self.container.exists(): self.container = bs.newnode('text', - attrs={ - 'text': final_text, - 'scale': 0.7, - 'color': (0.5, 1.0, 0.5), - 'h_attach': 'right', - 'v_attach': 'top', - 'h_align': 'left', - 'v_align': 'top', - 'position': (-220, -100), - 'flatness': 0.0, - 'shadow': 1.0, - 'res_scale': 1.5 - }) + attrs={ + 'text': final_text, + 'scale': 0.7, + 'color': (0.5, 1.0, 0.5), + 'h_attach': 'right', + 'v_attach': 'top', + 'h_align': 'left', + 'v_align': 'top', + 'position': (-220, -100), + 'flatness': 0.0, + 'shadow': 1.0, + 'res_scale': 1.5 + }) else: self.container.text = final_text From abe91c4b0e06345885e8d134970a8061302a192f Mon Sep 17 00:00:00 2001 From: DrapBS Date: Sat, 14 Feb 2026 10:07:07 +0100 Subject: [PATCH 10/84] Upload The plugin on plugin manager This plugin contains plugman dict --- plugins/utilities/players_list.py | 1276 +++++++++++++++++++++++++++-- 1 file changed, 1230 insertions(+), 46 deletions(-) diff --git a/plugins/utilities/players_list.py b/plugins/utilities/players_list.py index b97906b..37afb8e 100644 --- a/plugins/utilities/players_list.py +++ b/plugins/utilities/players_list.py @@ -1,61 +1,1245 @@ -# ======================== works only if you are host or plugin works on the server created On '2026/02/06' -# ======================== - +# Hard Work (took me 54 hours), and thx for Polish # ba_meta require api 9 -import bascenev1 as bs +""" +Players List v1.7 - An Live Players View. +""" + +from __future__ import annotations +from typing import TYPE_CHECKING import babase +import bauiv1 as bui +import bascenev1 as bs +from bauiv1 import ( + containerwidget as cw, + buttonwidget as bw, + textwidget as tw, + checkboxwidget as cbw, + scrollwidget as sw, + get_special_widget as gsw +) +from bauiv1lib.popup import PopupMenu +from colorsys import hsv_to_rgb -# ba_meta export babase.Plugin +if TYPE_CHECKING: + from typing import Any + +plugman = dict( + plugin_name="players_list", + description="A plugin that shows you live players list", + external_url="https://m.youtube.com/drap_bs", + authors=[ + {"name": "Drap", "email": "mrunittest@gmail.com", "discord": "drap_bs"} + ], + version="1.7.0", +) + +PLAYERS_ICON = "" -class ByDrap(bs.Plugin): +class ColorPickerWindow: + """Advanced color picker with custom RGB and preset colors""" + + # Preset colors grid (4 rows × 8 columns = 32 colors) + PRESET_COLORS = [ + # Row 1 - Reds to Greens + [(1.2, 0.0, 0.0), (0.65, 0.0, 0.0), (1.2, 0.8, 0.0), (0.89, 0.59, 0.0), (0.67, 1.0, 0.0), (0.52, 0.78, 0.0), (0.0, 1.2, 0.0), (0.0, 0.78, 0.0)], + # Row 2 - Cyans to Purples + [(0.0, 1.2, 0.8), (0.0, 0.78, 0.52), (0.0, 0.8, 1.2), (0.0, 0.52, 0.78), (0.0, 0.0, 1.2), (0.0, 0.0, 0.78), (0.8, 0.0, 1.2), (0.52, 0.0, 0.78)], + # Row 3 - Browns/Tans + [(1.0, 0.82, 0.48), (0.8, 0.62, 0.28), (0.8, 0.8, 0.0), (0.55, 0.55, 0.0)], + ] + + def __init__(self, callback, parent_window): + self._callback = callback + self._parent = parent_window + self._width = 1100 + self._height = 750 + + # Current RGB values + self._r = 1.25 + self._g = 1.25 + self._b = 1.25 + + # Load saved colors from config + cfg = babase.app.config + self._saved_colors = cfg.get('Players Display - Saved Colors', []) + + self._root_widget = cw( + parent=gsw('overlay_stack'), + size=(self._width, self._height), + transition='in_scale', + stack_offset=(250.0, -11.0), + color=(0.25, 0.25, 0.25) + ) + + # ═══════════════════════════════════════════════════ + # LEFT SECTION - Custom Color Creator + # ═══════════════════════════════════════════════════ + left_container = cw( + parent=self._root_widget, + position=(-531.9, 5.9), + size=(520.0, 750.0), + color=(0.25, 0.25, 0.25), + root_selectable=True + ) + + # "Custom" title + tw( + parent=self._root_widget, + position=(-306.8, 711.5), + size=(100, 30), + text='Custom', + color=(0.75, 0.75, 0.75), + scale=1.65 + ) + + # ═══ RGB Color Displays ═══ + # Red display + self._red_display = bw( + parent=self._root_widget, + position=(-481.5, 602.3), + size=(120.0, 85.0), + label='', + button_type='square', + color=(self._r, 0, 0), + enable_sound=False + ) + + # Green display + self._green_display = bw( + parent=self._root_widget, + position=(-481.5, 516.3), + size=(120.0, 85.0), + label='', + button_type='square', + color=(0, self._g, 0), + enable_sound=False + ) + + # Blue display + self._blue_display = bw( + parent=self._root_widget, + position=(-481.5, 429.3), + size=(120.0, 85.0), + label='', + button_type='square', + color=(0, 0, self._b), + enable_sound=False + ) + + # Preview display (gray box) + self._preview_display = bw( + parent=self._root_widget, + position=(-481.5, 316.3), + size=(120.0, 85.0), + label='', + button_type='square', + color=(self._r, self._g, self._b), + enable_sound=False + ) + + # ═══ RGB Control Buttons ═══ + # RED controls + bw( + parent=self._root_widget, + position=(-358.4, 599.3), + size=(150.0, 90.0), + label='+', + button_type='square', + textcolor=(0.75, 0.0, 0.0), + text_scale=2, + color=(0.15, 0.1, 0.1), + on_activate_call=lambda: self._adjust_color('r', 0.05) + ) + + bw( + parent=self._root_widget, + position=(-208.4, 599.3), + size=(130.0, 90.0), + label='−', + button_type='square', + color=(0.15, 0.1, 0.1), + text_scale=2, + textcolor=(0.75, 0, 0), + on_activate_call=lambda: self._adjust_color('r', -0.05) + ) + + # GREEN controls + bw( + parent=self._root_widget, + position=(-358.4, 509.3), + size=(150.0, 90.0), + label='+', + button_type='square', + textcolor=(0, 0.75, 0.0), + text_scale=2, + color=(0.1, 0.15, 0.1), + on_activate_call=lambda: self._adjust_color('g', 0.05) + ) + + bw( + parent=self._root_widget, + position=(-208.4, 509.3), + size=(130.0, 90.0), + label='−', + button_type='square', + color=(0.1, 0.15, 0.1), + text_scale=2, + textcolor=(0, 0.75, 0), + on_activate_call=lambda: self._adjust_color('g', -0.05) + ) + + # BLUE controls + bw( + parent=self._root_widget, + position=(-358.4, 419.3), + size=(150.0, 90.0), + label='+', + button_type='square', + textcolor=(0, 0.0, 0.75), + text_scale=2, + color=(0.1, 0.1, 0.15), + on_activate_call=lambda: self._adjust_color('b', 0.05) + ) + + bw( + parent=self._root_widget, + position=(-208.4, 418.3), + size=(130.0, 90.0), + label='−', + button_type='square', + color=(0.1, 0.1, 0.15), + text_scale=2, + textcolor=(0, 0, 0.75), + on_activate_call=lambda: self._adjust_color('b', -0.05) + ) + + # ═══ RGB Value Displays ═══ + self._r_text = tw( + parent=self._root_widget, + position=(-343.5, 343.4), + size=(100, 30), + text=f'{self._r:.2f}', + color=(1.0, 0.0, 0.0) + ) + + self._g_text = tw( + parent=self._root_widget, + position=(-253.5, 343.4), + size=(100, 30), + text=f'{self._g:.2f}', + color=(0.0, 1.0, 0.0) + ) + + self._b_text = tw( + parent=self._root_widget, + position=(-153.5, 343.4), + size=(100, 30), + text=f'{self._b:.2f}', + color=(0.0, 0.0, 1.0) + ) + + # ═══ Separator Lines ═══ + bui.imagewidget( + texture=bui.gettexture('white'), + opacity=0.6, + position=(-460.6, 406.2), + size=(370.0, 4.0), + parent=self._root_widget, + color=(0.4, 0.4, 0.4) + ) + + bui.imagewidget( + texture=bui.gettexture('white'), + opacity=0.6, + position=(-478.6, 246.2), + size=(400.0, 4.0), + parent=self._root_widget, + color=(0.4, 0.4, 0.4) + ) + + bui.imagewidget( + texture=bui.gettexture('white'), + opacity=0.2, + position=(-281.3, 250.8), + size=(4.0, 52.0), + parent=self._root_widget + ) + + # ═══ Action Buttons ═══ + # Save button + bw( + parent=self._root_widget, + position=(-483.2, 255.0), + size=(200.0, 45.0), + label='Save', + button_type='square', + textcolor=(1.2, 1.2, 1.2), + color=(0.0, 0.89, 0.0), + on_activate_call=self._save_color + ) + + # Apply button + bw( + parent=self._root_widget, + position=(-271.2, 256.0), + size=(200.0, 45.0), + label='Apply', + button_type='square', + textcolor=(1.2, 1.2, 1.2), + color=(0.0, 0.89, 0.0), + on_activate_call=self._apply_custom_color + ) + + # ═══ Saved Colors Section ═══ + tw( + parent=self._root_widget, + position=(-476.8, 212.5), + size=(100, 30), + text='Saved', + color=(0.65, 0.65, 0.65), + scale=1.2 + ) + + self._saved_scroll = bui.hscrollwidget( + parent=self._root_widget, + position=(-487.9, 63.4), + size=(420.0, 140.0), + capture_arrows=True, + border_opacity=1.2 + ) + + # ═══════════════════════════════════════════════════ + # RIGHT SECTION - Preset Colors + # ═══════════════════════════════════════════════════ + + # "Colors" title + tw( + parent=self._root_widget, + position=(193.2, 711.5), + size=(100, 30), + text='Colors', + color=(0.75, 0.75, 0.75), + scale=1.65 + ) + + tw( + parent=self._root_widget, + position=(273.2, 702.5), + size=(100, 30), + text='pick one for your list !', + color=(0.55, 0.55, 0.55), + scale=0.875 + ) + + # Separator line + bui.imagewidget( + texture=bui.gettexture('white'), + opacity=0.6, + position=(139.4, 396.2), + size=(750.0, 7.5), + parent=self._root_widget, + color=(0.4, 0.4, 0.4) + ) + + bui.imagewidget( + texture=bui.gettexture('white'), + opacity=0.2, + position=(495.7, 402.8), + size=(7.5, 87.5), + parent=self._root_widget + ) + + # Preset colors grid + self._create_preset_colors() + + # "More Colors Soon...." text + tw( + parent=self._root_widget, + position=(442.6, 347.5), + size=(100, 30), + text='More Colors Soon....', + color=(0.32, 0.32, 0.32), + shadow=False + ) + + # Loading spinner + bui.spinnerwidget( + parent=self._root_widget, + position=(554.3, 323.2), + size=45.0 + ) + + # Back button (top left corner) + bw( + parent=self._root_widget, + position=(-510, 690), + size=(60, 50), + label='<', + color=(0.6, 0.1, 0.1), + textcolor=(1, 0.8, 0.8), + text_scale=1.5, + on_activate_call=self._close + ) + + # Load saved colors + self._load_saved_colors() + + cw(edit=self._root_widget, on_cancel_call=self._close) + + def _load_saved_colors(self): + """Load and display saved colors from config""" + if not self._saved_colors: + return + + # Create container for saved colors + self._saved_container = bui.containerwidget( + parent=self._saved_scroll, + size=(max(1000, len(self._saved_colors) * 70), 100), + background=False + ) + + # Add all saved color buttons + for idx, color in enumerate(self._saved_colors): + x_pos = idx * 70 + bw( + parent=self._saved_container, + position=(x_pos, 10), + size=(60, 90), + label='', + button_type='square', + color=color, + on_activate_call=lambda c=color: self._select_saved_color(c) + ) + + def _create_preset_colors(self): + """Create preset color buttons grid""" + # Starting positions + start_x = 88.1 + button_size = 80.0 + spacing = 20.0 + + row_positions = [589.1, 499.1, 409.1] + + for row_idx, row_colors in enumerate(self.PRESET_COLORS): + y = row_positions[row_idx] + + for col_idx, color in enumerate(row_colors): + x = start_x + col_idx * (button_size + spacing) + + bw( + parent=self._root_widget, + position=(x, y), + size=(button_size, button_size), + label='', + button_type='square', + color=color, + on_activate_call=lambda c=color: self._select_preset_color(c) + ) + + def _adjust_color(self, channel: str, amount: float): + """Adjust RGB values""" + if channel == 'r': + self._r = max(0.0, min(1.5, self._r + amount)) + elif channel == 'g': + self._g = max(0.0, min(1.5, self._g + amount)) + elif channel == 'b': + self._b = max(0.0, min(1.5, self._b + amount)) + + self._update_displays() + bui.getsound('click01').play() + + def _update_displays(self): + """Update all color displays""" + # Update individual color displays + bw(edit=self._red_display, color=(self._r, 0, 0)) + bw(edit=self._green_display, color=(0, self._g, 0)) + bw(edit=self._blue_display, color=(0, 0, self._b)) + + # Update preview + bw(edit=self._preview_display, color=(self._r, self._g, self._b)) + + # Update RGB text displays + tw(edit=self._r_text, text=f'{self._r:.2f}') + tw(edit=self._g_text, text=f'{self._g:.2f}') + tw(edit=self._b_text, text=f'{self._b:.2f}') + + def _save_color(self): + """Save current color to saved list""" + current_color = (self._r, self._g, self._b) + self._saved_colors.append(current_color) + + # Save to config + cfg = babase.app.config + cfg['Players Display - Saved Colors'] = self._saved_colors + cfg.commit() + + # Create container for saved colors if doesn't exist + if not hasattr(self, '_saved_container'): + self._saved_container = bui.containerwidget( + parent=self._saved_scroll, + size=(1000, 100), + background=False + ) + + # Add color button in horizontal line + x_pos = (len(self._saved_colors) - 1) * 70 # 70 = button width + spacing + + saved_btn = bw( + parent=self._saved_container, + position=(x_pos, 10), + size=(60, 90), + label='', + button_type='square', + color=current_color, + on_activate_call=lambda c=current_color: self._select_saved_color(c) + ) + + bui.screenmessage('Saved!', color=(0, 1, 0)) + bui.getsound('cashRegister').play() + + def _select_saved_color(self, color: tuple): + """Select a saved color and apply it""" + # Update custom RGB values + self._r, self._g, self._b = color + self._update_displays() + + # Apply the color + self._callback(color) + bui.screenmessage('Applied!', color=(0, 1, 0)) + bui.getsound('ding').play() + + def _apply_custom_color(self): + """Apply the custom RGB color (don't close)""" + self._callback((self._r, self._g, self._b)) + bui.screenmessage('Applied!', color=(0, 1, 0)) + bui.getsound('dingSmall').play() + + def _select_preset_color(self, color: tuple): + """Select a preset color and close the window""" + self._callback(color) + self._close() # Close the window after selecting + + def _close(self): + cw(edit=self._root_widget, transition='out_scale') + + +class SettingsWindow: + """Main settings window with live preview""" + + # Design styles + DESIGNS = ['box', 'simple', 'minimal'] + + def __init__(self, source_widget=None): + self._width = 1150 + self._height = 700 + + # Get current settings + cfg = babase.app.config + self._temp_show_full = cfg.get('Players Display - Show Full Name', False) + self._temp_name_type = cfg.get('Players Display - Name Type', 'Player Name') + self._temp_color = cfg.get('Players Display - Color', (1, 1, 1)) + self._temp_rainbow = cfg.get('Players Display - Rainbow', False) + self._temp_show_id = cfg.get('Players Display - Show Client ID', True) + self._temp_style = cfg.get('Players Display - Style', 'box') + + self._root_widget = cw( + parent=gsw('overlay_stack'), + size=(self._width, self._height), + transition='in_scale', + stack_offset=(-250.0, -20.0), + color=(0.25, 0.25, 0.25) + ) + + # ═══════════════════════════════════════════════════ + # HEADER + # ═══════════════════════════════════════════════════ + + # Back button + bw( + parent=self._root_widget, + position=(9.6, 626.3), + size=(135.0, 120.0), + label='«', + color=(1.2, 0.0, 0.0), + textcolor=(1.5, 1.5, 1.5), + text_scale=2, + scale=0.75, + on_activate_call=self._close + ) + + # Settings title + tw( + parent=self._root_widget, + position=(188.3, 661.9), + size=(100, 30), + text='Settings', + color=(0.75, 0.75, 0.75), + scale=2 + ) + + tw( + parent=self._root_widget, + position=(318.3, 651.9), + size=(100, 30), + text='Customize Your Players List.', + color=(0.55, 0.55, 0.55), + scale=0.85, + shadow=0.4 + ) + + # Apply button + bw( + parent=self._root_widget, + position=(877.9, 639.1), + size=(185.0, 100.0), + label='Apply', + color=(0.0, 0.89, 0.0), + textcolor=(1, 1, 1), + on_activate_call=self._apply_changes + ) + + # ═══════════════════════════════════════════════════ + # MAIN SCROLL AREA + # ═══════════════════════════════════════════════════ + + scroll = sw( + parent=self._root_widget, + position=(86.8, 61.7), + size=(1000.0, 569.0), + border_opacity=1.25 + ) + + # ═══════════════════════════════════════════════════ + # SHOW PLAYER SECTION + # ═══════════════════════════════════════════════════ + + tw( + parent=self._root_widget, + position=(111.4, 581.4), + size=(100, 30), + text='Show Player', + scale=1.25, + color=(0.85, 0.85, 0.85) + ) + + # Separator line + bui.imagewidget( + texture=bui.gettexture('white'), + opacity=0.6, + position=(94.3, 480.9), + size=(975.0, 4.0), + parent=self._root_widget + ) + + # Name type label + tw( + parent=self._root_widget, + position=(95, 535), + size=(0, 0), + text='Name Type:', + color=(0.85, 0.85, 0.85), + h_align='left', + v_align='center', + scale=0.8 + ) + + # Popup menu for name type selection + PopupMenu( + parent=self._root_widget, + position=(95, 487), + choices=['Account Name', 'Player Name'], + current_choice=self._temp_name_type, + on_value_change_call=self._set_name_type, + button_size=(185, 45) + ) + + # Vertical separator 1 + bui.imagewidget( + texture=bui.gettexture('white'), + opacity=0.45, + position=(275.8, 483.6), + size=(4.0, 90.0), + parent=self._root_widget + ) + + # Full Name checkbox (shows both names) + cbw( + parent=self._root_widget, + position=(288.0, 513.1), + size=(100, 30), + text='Full Name', + color=(0.0, 0.89, 0.0), + scale=1.2, + textcolor=(1, 1, 1), + value=self._temp_show_full, + on_value_change_call=self._on_full_name_change + ) + + # Vertical separator 2 + bui.imagewidget( + texture=bui.gettexture('white'), + opacity=0.45, + position=(495.8, 483.6), + size=(4.0, 90.0), + parent=self._root_widget + ) + + # Client ID checkbox + cbw( + parent=self._root_widget, + position=(509.9, 510.8), + size=(100, 30), + text='Client ID', + textcolor=(1, 1, 1.0), + color=(0.05, 0.78, 0.05), + scale=1.25, + value=self._temp_show_id, + on_value_change_call=self._on_client_id_change + ) + + # ═══════════════════════════════════════════════════ + # COLOR SECTION + # ═══════════════════════════════════════════════════ + + tw( + parent=self._root_widget, + position=(108.3, 431.9), + size=(100, 30), + text='Color', + color=(0.75, 0.75, 0.75), + scale=1.35 + ) + + # Separator line + bui.imagewidget( + texture=bui.gettexture('white'), + opacity=0.6, + position=(94.3, 336.9), + size=(975.0, 2.5), + parent=self._root_widget + ) + + # Color display button + self._color_display = bw( + parent=self._root_widget, + position=(97.0, 341.5), + size=(85.0, 85.0), + label='', + color=self._temp_color, + enable_sound=False + ) + + # Vertical separator + bui.imagewidget( + texture=bui.gettexture('white'), + opacity=0.45, + position=(192.8, 338.6), + size=(5.0, 80.0), + parent=self._root_widget + ) + + # Pick button + bw( + parent=self._root_widget, + position=(198.6, 343.3), + size=(160.0, 80.0), + label='Pick', + button_type='square', + color=(0.0, 0.0, 0.3555), + textcolor=(0.9, 0.9, 1), + on_activate_call=self._open_color_picker + ) + + # Vertical separator + bui.imagewidget( + texture=bui.gettexture('white'), + opacity=0.45, + position=(362.8, 338.6), + size=(5.0, 80.0), + parent=self._root_widget + ) + + # Rainbow checkbox + cbw( + parent=self._root_widget, + position=(378.0, 353.1), + size=(100, 30), + text='Rainbow', + color=(0.0, 0.89, 0.0), + scale=1.2, + textcolor=(1, 1, 1), + value=self._temp_rainbow, + on_value_change_call=self._on_rainbow_change + ) + + # ═══════════════════════════════════════════════════ + # DESIGN SECTION + # ═══════════════════════════════════════════════════ + + tw( + parent=self._root_widget, + position=(108.3, 301.9), + size=(100, 30), + text='Design', + color=(0.75, 0.75, 0.75), + scale=1.35 + ) + + # Design buttons (3 styles) + self._design_btns = [] + + # Box style + btn1 = bw( + parent=self._root_widget, + position=(84.2, 63.8), + size=(350.0, 225.0), + label='BOX Style', + button_type='square', + color=(0.1, 1.2, 0.1) if self._temp_style == 'box' else (0.75, 0.85, 0.75), + on_activate_call=lambda: self._set_style('box') + ) + self._design_btns.append(btn1) + + # Simple style + btn2 = bw( + parent=self._root_widget, + position=(417.2, 63.8), + size=(350.0, 225.0), + label='Simple Style', + button_type='square', + color=(0.1, 1.2, 0.1) if self._temp_style == 'simple' else (0.75, 0.85, 0.75), + on_activate_call=lambda: self._set_style('simple') + ) + self._design_btns.append(btn2) + + # Minimal style + btn3 = bw( + parent=self._root_widget, + position=(744.2, 63.8), + size=(350.0, 225.0), + label='Minimal Style', + button_type='square', + color=(0.1, 1.2, 0.1) if self._temp_style == 'minimal' else (0.75, 0.85, 0.75), + on_activate_call=lambda: self._set_style('minimal') + ) + self._design_btns.append(btn3) + + # ═══════════════════════════════════════════════════ + # RESULT SECTION (Preview) + # ═══════════════════════════════════════════════════ + + result_container = cw( + parent=self._root_widget, + position=(1147.2, 121.9), + size=(450.0, 500.0), + color=(0.25, 0.25, 0.25) + ) + + tw( + parent=self._root_widget, + position=(1328.3, 581.9), + size=(100, 30), + text='Result', + color=(0.75, 0.75, 0.75), + scale=1.55 + ) + + # Preview text widget + self._preview_text = tw( + parent=result_container, + position=(225, 350), + size=(0, 0), + text=self._generate_preview_text(), + color=self._temp_color, + h_align='center', + v_align='center', + scale=0.5, + maxwidth=400 + ) + + # Start preview update loop + self._update_preview() + + cw(edit=self._root_widget, on_cancel_call=self._close) + + def _generate_preview_text(self): + """Generate preview text based on current settings""" + player_count = 3 # Sample count + + # Get icon + icon_text = f"{PLAYERS_ICON} " if PLAYERS_ICON else "" + + """Generate preview text based on current settings""" + player_count = 3 # Sample count + + if self._temp_style == 'box': + header = "╔═══════════════════╗\n" + header += f"║ PLAYERS ({player_count}) ║\n" + header += "╠═══════════════════╣\n\n" + footer = "\n\n╚═══════════════════╝" + elif self._temp_style == 'simple': + header = "━━━━━━━━━━━━━━━━━━━\n" + header += f" PLAYERS ({player_count})\n" + header += "━━━━━━━━━━━━━━━━━━━\n\n" + footer = "\n\n━━━━━━━━━━━━━━━━━━━" + else: # minimal + header = f" PLAYERS ({player_count})\n\n" + footer = "" + + # Sample names + player_name = "YT!Drap" + account_name = "Drap" + + # Build player line based on settings + if self._temp_show_full: + # Full name: show both + name_part = f"{player_name} | {account_name}" + else: + # Show based on name type + if self._temp_name_type == 'Account Name': + name_part = account_name + else: + name_part = player_name + + # Add client ID if enabled + if self._temp_show_id: + player_line = f" • {name_part} [#42]" + else: + player_line = f" • {name_part}" + + return header + player_line + footer + + def _update_preview(self): + """Update preview with rainbow effect if enabled""" + if not hasattr(self, '_preview_text') or not self._preview_text.exists(): + return + + # Update preview text + tw(edit=self._preview_text, text=self._generate_preview_text()) + + # Update color (rainbow or static) + if self._temp_rainbow: + # Smooth rainbow loop + if not hasattr(self, '_rainbow_hue'): + self._rainbow_hue = 0.0 + + self._rainbow_hue = (self._rainbow_hue + 0.02) % 1.0 + color = hsv_to_rgb(self._rainbow_hue, 1.0, 1.0) + tw(edit=self._preview_text, color=color) + else: + tw(edit=self._preview_text, color=self._temp_color) + + # Schedule next update + babase.apptimer(0.05, babase.WeakCall(self._update_preview)) + + def _set_name_type(self, name_type: str): + """Set name type""" + self._temp_name_type = name_type + + def _set_style(self, style: str): + """Set design style and update button colors""" + self._temp_style = style + + # Update all design buttons + for idx, btn in enumerate(self._design_btns): + if self.DESIGNS[idx] == style: + bw(edit=btn, color=(0.1, 1.2, 0.1)) + else: + bw(edit=btn, color=(0.75, 0.85, 0.75)) + + def _on_full_name_change(self, value: bool): + self._temp_show_full = value + + def _on_client_id_change(self, value: bool): + self._temp_show_id = value + + def _on_rainbow_change(self, value: bool): + self._temp_rainbow = value + + def _on_color_selected(self, color: tuple): + self._temp_color = color + bw(edit=self._color_display, color=color) + + def _open_color_picker(self): + ColorPickerWindow(self._on_color_selected, self) + + def _apply_changes(self): + """Apply all changes and save to config""" + cfg = babase.app.config + cfg['Players Display - Show Full Name'] = self._temp_show_full + cfg['Players Display - Name Type'] = self._temp_name_type + cfg['Players Display - Color'] = self._temp_color + cfg['Players Display - Rainbow'] = self._temp_rainbow + cfg['Players Display - Show Client ID'] = self._temp_show_id + cfg['Players Display - Style'] = self._temp_style + cfg.commit() + + bui.screenmessage('Settings Applied!', color=(0, 1, 0)) + bui.getsound('cashRegister').play() + + def _close(self): + cw(edit=self._root_widget, transition='out_scale') + + +class PlayersDisplay: + """Main display class""" + def __init__(self): self.container = None - self.timer = None - + self.rainbow_hue = 0.0 + self.current_players = {} # Track: {client_id: (player_name, account_name)} + def on_app_running(self): - self.timer = bs.AppTimer(1.0, bs.WeakCall(self._update_list), repeat=True) - - def has_settings_ui(self): - return True - - def show_settings_ui(self, source_widget): - babase.screenmessage("You tapped my settings!") - - def _update_list(self): - activity = bs.get_foreground_host_activity() - + self._update_loop() + + def _update_loop(self): + self._update_display() + + # Check if rainbow mode is enabled + cfg = babase.app.config + is_rainbow = cfg.get('Players Display - Rainbow', False) + + # Use faster loop for smooth rainbow, slower for normal updates + delay = 0.05 if is_rainbow else 0.5 + babase.apptimer(delay, babase.WeakCall(self._update_loop)) + + def _get_display_color(self): + cfg = babase.app.config + + if cfg.get('Players Display - Rainbow', False): + # Smooth rainbow loop with smaller increment + self.rainbow_hue = (self.rainbow_hue + 0.01) % 1.0 + return hsv_to_rgb(self.rainbow_hue, 1.0, 1.0) + else: + return cfg.get('Players Display - Color', (0.7, 0.7, 0.7)) + + def _show_message(self, message: str, color: tuple, duration: float = 1.0): + """Show a temporary message with fade in/out animation""" + try: + activity = bs.get_foreground_host_activity() + if activity is None: + return + + with activity.context: + # Create message node + msg_node = bs.newnode( + 'text', + attrs={ + 'text': message, + 'scale': 0.65, + 'color': color, + 'h_attach': 'right', + 'v_attach': 'top', + 'h_align': 'left', + 'v_align': 'top', + 'position': (-240, -270), # Below the main list + 'flatness': 0.0, + 'shadow': 1.2, + 'opacity': 0.0 # Start invisible + } + ) + + # Fade in animation (0.2 seconds) + bs.animate(msg_node, 'opacity', { + 0.0: 0.0, + 0.2: 0.95 + }) + + # Fade out animation (starts after duration) + bs.animate(msg_node, 'opacity', { + duration: 0.95, + duration + 0.3: 0.0 + }) + + # Delete node after animations complete + bs.timer(duration + 0.4, msg_node.delete) + + except Exception: + pass + + def _check_player_changes(self, new_players: dict): + """Check for players joining or leaving + new_players: {client_id: (player_name, account_name)} + """ + new_ids = set(new_players.keys()) + old_ids = set(self.current_players.keys()) + + # Players who joined + joined = new_ids - old_ids + for player_id in joined: + player_name, account_name = new_players[player_id] + message = f"{account_name} joined the server" + color = self._get_display_color() + self._show_message(message, color, duration=1.0) + + # Players who left + left = old_ids - new_ids + for player_id in left: + player_name, account_name = self.current_players[player_id] + message = f"{player_name} left the server" + color = (1.2, 0.2, 0.2) # Glowing red + self._show_message(message, color, duration=1.0) + + # Update current players + self.current_players = new_players.copy() + + def _update_display(self): + try: + activity = bs.get_foreground_host_activity() + except Exception: + activity = None + if activity is None: - if self.container: - self.container.delete() + if self.container is not None: + try: + self.container.delete() + except Exception: + pass self.container = None return - + + cfg = babase.app.config + show_full = cfg.get('Players Display - Show Full Name', False) + name_type = cfg.get('Players Display - Name Type', 'Player Name') + show_client_id = cfg.get('Players Display - Show Client ID', True) + style = cfg.get('Players Display - Style', 'box') + player_names = [] + new_players_data = {} # {client_id: (player_name, account_name)} + for p in activity.players: - name = p.getname(full=True) - player_names.append(f" • {name}") - - header = "┏━━━━ Players  ━━━━┓\n\n" - footer = "\n\n┗━━━━━━━━━━━━━━━┛" - players_str = "\n".join(player_names) if player_names else " Wait for players..." + try: + # Get both player name and account name + player_name = p.getname(full=True) + + try: + account_name = p.sessionplayer.inputdevice.get_v1_account_name(full=True) + if not account_name: + account_name = player_name + except Exception: + account_name = player_name + + # Get client ID + try: + client_id = p.sessionplayer.inputdevice.client_id + except Exception: + client_id = id(p) # Fallback to object id + + # Store for tracking + new_players_data[client_id] = (player_name, account_name) + + # Build name based on settings + if show_full: + # Full name: show both names + if player_name != account_name: + name = f"{player_name} | {account_name}" + else: + name = player_name + else: + # Show based on name type + if name_type == 'Account Name': + name = account_name + else: + name = player_name + + # Add client ID if enabled + if show_client_id: + try: + player_names.append(f" • {name} [#{client_id}]") + except Exception: + player_names.append(f" • {name}") + else: + player_names.append(f" • {name}") + + except Exception: + continue + + # Check for player changes (join/leave) + self._check_player_changes(new_players_data) + + # Get player count + player_count = len(player_names) + + # Get icon + icon_text = f"{PLAYERS_ICON} " if PLAYERS_ICON else "" + + # Different styles with player count + if style == 'box': + header = "╔═══════════════════╗\n" + header += f"║  PLAYERS ({player_count}) ║\n" + header += "╠═══════════════════╣\n\n" + footer = "\n\n╚═══════════════════╝" + elif style == 'simple': + header = "━━━━━━━━━━━━━━━━━━━\n" + header += f"{icon_text}PLAYERS ({player_count})\n" + header += "━━━━━━━━━━━━━━━━━━━\n\n" + footer = "\n\n━━━━━━━━━━━━━━━━━━━" + else: # minimal + header = f" PLAYERS ({player_count})\n\n" + footer = "" + + if player_names: + players_str = "\n".join(player_names) + else: + players_str = " ⏳ Waiting...\n No players yet" + final_text = header + players_str + footer + current_color = self._get_display_color() + + try: + with activity.context: + if self.container is None or not self.container.exists(): + self.container = bs.newnode( + 'text', + attrs={ + 'text': final_text, + 'scale': 0.7, + 'color': current_color, + 'h_attach': 'right', + 'v_attach': 'top', + 'h_align': 'left', + 'v_align': 'top', + 'position': (-240, -90), + 'flatness': 0.0, + 'shadow': 1.2, + 'opacity': 0.95 + } + ) + else: + self.container.text = final_text + self.container.color = current_color + except Exception: + pass - with activity.context: - if self.container is None or not self.container.exists(): - self.container = bs.newnode('text', - attrs={ - 'text': final_text, - 'scale': 0.7, - 'color': (0.5, 1.0, 0.5), - 'h_attach': 'right', - 'v_attach': 'top', - 'h_align': 'left', - 'v_align': 'top', - 'position': (-220, -100), - 'flatness': 0.0, - 'shadow': 1.0, - 'res_scale': 1.5 - }) - else: - self.container.text = final_text +# My First Cool Mod ,WooHoo. +# ba_meta export babase.Plugin +class CoolList(babase.Plugin): + """Main plugin class""" + + def __init__(self): + cfg = babase.app.config + + if 'Players Display - Show Full Name' not in cfg: + cfg['Players Display - Show Full Name'] = False # Default to single name + if 'Players Display - Name Type' not in cfg: + cfg['Players Display - Name Type'] = 'Player Name' # Default to player name + if 'Players Display - Color' not in cfg: + cfg['Players Display - Color'] = (1, 1, 1) + if 'Players Display - Rainbow' not in cfg: + cfg['Players Display - Rainbow'] = False + if 'Players Display - Show Client ID' not in cfg: + cfg['Players Display - Show Client ID'] = True + if 'Players Display - Style' not in cfg: + cfg['Players Display - Style'] = 'box' + + cfg.commit() + + self.display = PlayersDisplay() + self.display.on_app_running() + + def has_settings_ui(self) -> bool: + return True + + def show_settings_ui(self, source_widget: Any) -> None: + SettingsWindow(source_widget) From 0d39f577f2e0d49254db970da14bbe23ac0906ba Mon Sep 17 00:00:00 2001 From: DrapBS <260050423+DrapBS@users.noreply.github.com> Date: Sat, 14 Feb 2026 09:07:30 +0000 Subject: [PATCH 11/84] [ci] apply-plugin-metadata-and-formatting --- plugins/utilities.json | 14 ++ plugins/utilities/players_list.py | 342 +++++++++++++++--------------- 2 files changed, 187 insertions(+), 169 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 2d97a60..e13632d 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2407,6 +2407,20 @@ "md5sum": "5082acb27f3dbe701d8bd42d8948b352" } } + }, + "players_list": { + "description": "A plugin that shows you live players list", + "external_url": "https://m.youtube.com/drap_bs", + "authors": [ + { + "name": "Drap", + "email": "mrunittest@gmail.com", + "discord": "drap_bs" + } + ], + "versions": { + "1.7.0": null + } } } } \ No newline at end of file diff --git a/plugins/utilities/players_list.py b/plugins/utilities/players_list.py index 37afb8e..82c62ec 100644 --- a/plugins/utilities/players_list.py +++ b/plugins/utilities/players_list.py @@ -38,32 +38,34 @@ PLAYERS_ICON = "" class ColorPickerWindow: """Advanced color picker with custom RGB and preset colors""" - + # Preset colors grid (4 rows × 8 columns = 32 colors) PRESET_COLORS = [ # Row 1 - Reds to Greens - [(1.2, 0.0, 0.0), (0.65, 0.0, 0.0), (1.2, 0.8, 0.0), (0.89, 0.59, 0.0), (0.67, 1.0, 0.0), (0.52, 0.78, 0.0), (0.0, 1.2, 0.0), (0.0, 0.78, 0.0)], + [(1.2, 0.0, 0.0), (0.65, 0.0, 0.0), (1.2, 0.8, 0.0), (0.89, 0.59, 0.0), + (0.67, 1.0, 0.0), (0.52, 0.78, 0.0), (0.0, 1.2, 0.0), (0.0, 0.78, 0.0)], # Row 2 - Cyans to Purples - [(0.0, 1.2, 0.8), (0.0, 0.78, 0.52), (0.0, 0.8, 1.2), (0.0, 0.52, 0.78), (0.0, 0.0, 1.2), (0.0, 0.0, 0.78), (0.8, 0.0, 1.2), (0.52, 0.0, 0.78)], + [(0.0, 1.2, 0.8), (0.0, 0.78, 0.52), (0.0, 0.8, 1.2), (0.0, 0.52, 0.78), + (0.0, 0.0, 1.2), (0.0, 0.0, 0.78), (0.8, 0.0, 1.2), (0.52, 0.0, 0.78)], # Row 3 - Browns/Tans [(1.0, 0.82, 0.48), (0.8, 0.62, 0.28), (0.8, 0.8, 0.0), (0.55, 0.55, 0.0)], ] - + def __init__(self, callback, parent_window): self._callback = callback self._parent = parent_window self._width = 1100 self._height = 750 - + # Current RGB values self._r = 1.25 self._g = 1.25 self._b = 1.25 - + # Load saved colors from config cfg = babase.app.config self._saved_colors = cfg.get('Players Display - Saved Colors', []) - + self._root_widget = cw( parent=gsw('overlay_stack'), size=(self._width, self._height), @@ -71,7 +73,7 @@ class ColorPickerWindow: stack_offset=(250.0, -11.0), color=(0.25, 0.25, 0.25) ) - + # ═══════════════════════════════════════════════════ # LEFT SECTION - Custom Color Creator # ═══════════════════════════════════════════════════ @@ -82,7 +84,7 @@ class ColorPickerWindow: color=(0.25, 0.25, 0.25), root_selectable=True ) - + # "Custom" title tw( parent=self._root_widget, @@ -92,7 +94,7 @@ class ColorPickerWindow: color=(0.75, 0.75, 0.75), scale=1.65 ) - + # ═══ RGB Color Displays ═══ # Red display self._red_display = bw( @@ -104,7 +106,7 @@ class ColorPickerWindow: color=(self._r, 0, 0), enable_sound=False ) - + # Green display self._green_display = bw( parent=self._root_widget, @@ -115,7 +117,7 @@ class ColorPickerWindow: color=(0, self._g, 0), enable_sound=False ) - + # Blue display self._blue_display = bw( parent=self._root_widget, @@ -126,7 +128,7 @@ class ColorPickerWindow: color=(0, 0, self._b), enable_sound=False ) - + # Preview display (gray box) self._preview_display = bw( parent=self._root_widget, @@ -137,7 +139,7 @@ class ColorPickerWindow: color=(self._r, self._g, self._b), enable_sound=False ) - + # ═══ RGB Control Buttons ═══ # RED controls bw( @@ -151,7 +153,7 @@ class ColorPickerWindow: color=(0.15, 0.1, 0.1), on_activate_call=lambda: self._adjust_color('r', 0.05) ) - + bw( parent=self._root_widget, position=(-208.4, 599.3), @@ -163,7 +165,7 @@ class ColorPickerWindow: textcolor=(0.75, 0, 0), on_activate_call=lambda: self._adjust_color('r', -0.05) ) - + # GREEN controls bw( parent=self._root_widget, @@ -176,7 +178,7 @@ class ColorPickerWindow: color=(0.1, 0.15, 0.1), on_activate_call=lambda: self._adjust_color('g', 0.05) ) - + bw( parent=self._root_widget, position=(-208.4, 509.3), @@ -188,7 +190,7 @@ class ColorPickerWindow: textcolor=(0, 0.75, 0), on_activate_call=lambda: self._adjust_color('g', -0.05) ) - + # BLUE controls bw( parent=self._root_widget, @@ -201,7 +203,7 @@ class ColorPickerWindow: color=(0.1, 0.1, 0.15), on_activate_call=lambda: self._adjust_color('b', 0.05) ) - + bw( parent=self._root_widget, position=(-208.4, 418.3), @@ -213,7 +215,7 @@ class ColorPickerWindow: textcolor=(0, 0, 0.75), on_activate_call=lambda: self._adjust_color('b', -0.05) ) - + # ═══ RGB Value Displays ═══ self._r_text = tw( parent=self._root_widget, @@ -222,7 +224,7 @@ class ColorPickerWindow: text=f'{self._r:.2f}', color=(1.0, 0.0, 0.0) ) - + self._g_text = tw( parent=self._root_widget, position=(-253.5, 343.4), @@ -230,7 +232,7 @@ class ColorPickerWindow: text=f'{self._g:.2f}', color=(0.0, 1.0, 0.0) ) - + self._b_text = tw( parent=self._root_widget, position=(-153.5, 343.4), @@ -238,7 +240,7 @@ class ColorPickerWindow: text=f'{self._b:.2f}', color=(0.0, 0.0, 1.0) ) - + # ═══ Separator Lines ═══ bui.imagewidget( texture=bui.gettexture('white'), @@ -248,7 +250,7 @@ class ColorPickerWindow: parent=self._root_widget, color=(0.4, 0.4, 0.4) ) - + bui.imagewidget( texture=bui.gettexture('white'), opacity=0.6, @@ -257,7 +259,7 @@ class ColorPickerWindow: parent=self._root_widget, color=(0.4, 0.4, 0.4) ) - + bui.imagewidget( texture=bui.gettexture('white'), opacity=0.2, @@ -265,7 +267,7 @@ class ColorPickerWindow: size=(4.0, 52.0), parent=self._root_widget ) - + # ═══ Action Buttons ═══ # Save button bw( @@ -278,7 +280,7 @@ class ColorPickerWindow: color=(0.0, 0.89, 0.0), on_activate_call=self._save_color ) - + # Apply button bw( parent=self._root_widget, @@ -290,7 +292,7 @@ class ColorPickerWindow: color=(0.0, 0.89, 0.0), on_activate_call=self._apply_custom_color ) - + # ═══ Saved Colors Section ═══ tw( parent=self._root_widget, @@ -300,7 +302,7 @@ class ColorPickerWindow: color=(0.65, 0.65, 0.65), scale=1.2 ) - + self._saved_scroll = bui.hscrollwidget( parent=self._root_widget, position=(-487.9, 63.4), @@ -308,11 +310,11 @@ class ColorPickerWindow: capture_arrows=True, border_opacity=1.2 ) - + # ═══════════════════════════════════════════════════ # RIGHT SECTION - Preset Colors # ═══════════════════════════════════════════════════ - + # "Colors" title tw( parent=self._root_widget, @@ -322,7 +324,7 @@ class ColorPickerWindow: color=(0.75, 0.75, 0.75), scale=1.65 ) - + tw( parent=self._root_widget, position=(273.2, 702.5), @@ -331,7 +333,7 @@ class ColorPickerWindow: color=(0.55, 0.55, 0.55), scale=0.875 ) - + # Separator line bui.imagewidget( texture=bui.gettexture('white'), @@ -341,7 +343,7 @@ class ColorPickerWindow: parent=self._root_widget, color=(0.4, 0.4, 0.4) ) - + bui.imagewidget( texture=bui.gettexture('white'), opacity=0.2, @@ -349,10 +351,10 @@ class ColorPickerWindow: size=(7.5, 87.5), parent=self._root_widget ) - + # Preset colors grid self._create_preset_colors() - + # "More Colors Soon...." text tw( parent=self._root_widget, @@ -362,14 +364,14 @@ class ColorPickerWindow: color=(0.32, 0.32, 0.32), shadow=False ) - + # Loading spinner bui.spinnerwidget( parent=self._root_widget, position=(554.3, 323.2), size=45.0 ) - + # Back button (top left corner) bw( parent=self._root_widget, @@ -381,24 +383,24 @@ class ColorPickerWindow: text_scale=1.5, on_activate_call=self._close ) - + # Load saved colors self._load_saved_colors() - + cw(edit=self._root_widget, on_cancel_call=self._close) - + def _load_saved_colors(self): """Load and display saved colors from config""" if not self._saved_colors: return - + # Create container for saved colors self._saved_container = bui.containerwidget( parent=self._saved_scroll, size=(max(1000, len(self._saved_colors) * 70), 100), background=False ) - + # Add all saved color buttons for idx, color in enumerate(self._saved_colors): x_pos = idx * 70 @@ -411,22 +413,22 @@ class ColorPickerWindow: color=color, on_activate_call=lambda c=color: self._select_saved_color(c) ) - + def _create_preset_colors(self): """Create preset color buttons grid""" # Starting positions start_x = 88.1 button_size = 80.0 spacing = 20.0 - + row_positions = [589.1, 499.1, 409.1] - + for row_idx, row_colors in enumerate(self.PRESET_COLORS): y = row_positions[row_idx] - + for col_idx, color in enumerate(row_colors): x = start_x + col_idx * (button_size + spacing) - + bw( parent=self._root_widget, position=(x, y), @@ -436,7 +438,7 @@ class ColorPickerWindow: color=color, on_activate_call=lambda c=color: self._select_preset_color(c) ) - + def _adjust_color(self, channel: str, amount: float): """Adjust RGB values""" if channel == 'r': @@ -445,35 +447,35 @@ class ColorPickerWindow: self._g = max(0.0, min(1.5, self._g + amount)) elif channel == 'b': self._b = max(0.0, min(1.5, self._b + amount)) - + self._update_displays() bui.getsound('click01').play() - + def _update_displays(self): """Update all color displays""" # Update individual color displays bw(edit=self._red_display, color=(self._r, 0, 0)) bw(edit=self._green_display, color=(0, self._g, 0)) bw(edit=self._blue_display, color=(0, 0, self._b)) - + # Update preview bw(edit=self._preview_display, color=(self._r, self._g, self._b)) - + # Update RGB text displays tw(edit=self._r_text, text=f'{self._r:.2f}') tw(edit=self._g_text, text=f'{self._g:.2f}') tw(edit=self._b_text, text=f'{self._b:.2f}') - + def _save_color(self): """Save current color to saved list""" current_color = (self._r, self._g, self._b) self._saved_colors.append(current_color) - + # Save to config cfg = babase.app.config cfg['Players Display - Saved Colors'] = self._saved_colors cfg.commit() - + # Create container for saved colors if doesn't exist if not hasattr(self, '_saved_container'): self._saved_container = bui.containerwidget( @@ -481,10 +483,10 @@ class ColorPickerWindow: size=(1000, 100), background=False ) - + # Add color button in horizontal line x_pos = (len(self._saved_colors) - 1) * 70 # 70 = button width + spacing - + saved_btn = bw( parent=self._saved_container, position=(x_pos, 10), @@ -494,46 +496,46 @@ class ColorPickerWindow: color=current_color, on_activate_call=lambda c=current_color: self._select_saved_color(c) ) - + bui.screenmessage('Saved!', color=(0, 1, 0)) bui.getsound('cashRegister').play() - + def _select_saved_color(self, color: tuple): """Select a saved color and apply it""" # Update custom RGB values self._r, self._g, self._b = color self._update_displays() - + # Apply the color self._callback(color) bui.screenmessage('Applied!', color=(0, 1, 0)) bui.getsound('ding').play() - + def _apply_custom_color(self): """Apply the custom RGB color (don't close)""" self._callback((self._r, self._g, self._b)) bui.screenmessage('Applied!', color=(0, 1, 0)) bui.getsound('dingSmall').play() - + def _select_preset_color(self, color: tuple): """Select a preset color and close the window""" self._callback(color) self._close() # Close the window after selecting - + def _close(self): cw(edit=self._root_widget, transition='out_scale') class SettingsWindow: """Main settings window with live preview""" - + # Design styles DESIGNS = ['box', 'simple', 'minimal'] - + def __init__(self, source_widget=None): self._width = 1150 self._height = 700 - + # Get current settings cfg = babase.app.config self._temp_show_full = cfg.get('Players Display - Show Full Name', False) @@ -542,7 +544,7 @@ class SettingsWindow: self._temp_rainbow = cfg.get('Players Display - Rainbow', False) self._temp_show_id = cfg.get('Players Display - Show Client ID', True) self._temp_style = cfg.get('Players Display - Style', 'box') - + self._root_widget = cw( parent=gsw('overlay_stack'), size=(self._width, self._height), @@ -550,11 +552,11 @@ class SettingsWindow: stack_offset=(-250.0, -20.0), color=(0.25, 0.25, 0.25) ) - + # ═══════════════════════════════════════════════════ # HEADER # ═══════════════════════════════════════════════════ - + # Back button bw( parent=self._root_widget, @@ -567,7 +569,7 @@ class SettingsWindow: scale=0.75, on_activate_call=self._close ) - + # Settings title tw( parent=self._root_widget, @@ -577,7 +579,7 @@ class SettingsWindow: color=(0.75, 0.75, 0.75), scale=2 ) - + tw( parent=self._root_widget, position=(318.3, 651.9), @@ -587,7 +589,7 @@ class SettingsWindow: scale=0.85, shadow=0.4 ) - + # Apply button bw( parent=self._root_widget, @@ -598,22 +600,22 @@ class SettingsWindow: textcolor=(1, 1, 1), on_activate_call=self._apply_changes ) - + # ═══════════════════════════════════════════════════ # MAIN SCROLL AREA # ═══════════════════════════════════════════════════ - + scroll = sw( parent=self._root_widget, position=(86.8, 61.7), size=(1000.0, 569.0), border_opacity=1.25 ) - + # ═══════════════════════════════════════════════════ # SHOW PLAYER SECTION # ═══════════════════════════════════════════════════ - + tw( parent=self._root_widget, position=(111.4, 581.4), @@ -622,7 +624,7 @@ class SettingsWindow: scale=1.25, color=(0.85, 0.85, 0.85) ) - + # Separator line bui.imagewidget( texture=bui.gettexture('white'), @@ -631,7 +633,7 @@ class SettingsWindow: size=(975.0, 4.0), parent=self._root_widget ) - + # Name type label tw( parent=self._root_widget, @@ -643,7 +645,7 @@ class SettingsWindow: v_align='center', scale=0.8 ) - + # Popup menu for name type selection PopupMenu( parent=self._root_widget, @@ -653,7 +655,7 @@ class SettingsWindow: on_value_change_call=self._set_name_type, button_size=(185, 45) ) - + # Vertical separator 1 bui.imagewidget( texture=bui.gettexture('white'), @@ -662,7 +664,7 @@ class SettingsWindow: size=(4.0, 90.0), parent=self._root_widget ) - + # Full Name checkbox (shows both names) cbw( parent=self._root_widget, @@ -675,7 +677,7 @@ class SettingsWindow: value=self._temp_show_full, on_value_change_call=self._on_full_name_change ) - + # Vertical separator 2 bui.imagewidget( texture=bui.gettexture('white'), @@ -684,7 +686,7 @@ class SettingsWindow: size=(4.0, 90.0), parent=self._root_widget ) - + # Client ID checkbox cbw( parent=self._root_widget, @@ -697,11 +699,11 @@ class SettingsWindow: value=self._temp_show_id, on_value_change_call=self._on_client_id_change ) - + # ═══════════════════════════════════════════════════ # COLOR SECTION # ═══════════════════════════════════════════════════ - + tw( parent=self._root_widget, position=(108.3, 431.9), @@ -710,7 +712,7 @@ class SettingsWindow: color=(0.75, 0.75, 0.75), scale=1.35 ) - + # Separator line bui.imagewidget( texture=bui.gettexture('white'), @@ -719,7 +721,7 @@ class SettingsWindow: size=(975.0, 2.5), parent=self._root_widget ) - + # Color display button self._color_display = bw( parent=self._root_widget, @@ -729,7 +731,7 @@ class SettingsWindow: color=self._temp_color, enable_sound=False ) - + # Vertical separator bui.imagewidget( texture=bui.gettexture('white'), @@ -738,7 +740,7 @@ class SettingsWindow: size=(5.0, 80.0), parent=self._root_widget ) - + # Pick button bw( parent=self._root_widget, @@ -750,7 +752,7 @@ class SettingsWindow: textcolor=(0.9, 0.9, 1), on_activate_call=self._open_color_picker ) - + # Vertical separator bui.imagewidget( texture=bui.gettexture('white'), @@ -759,7 +761,7 @@ class SettingsWindow: size=(5.0, 80.0), parent=self._root_widget ) - + # Rainbow checkbox cbw( parent=self._root_widget, @@ -772,11 +774,11 @@ class SettingsWindow: value=self._temp_rainbow, on_value_change_call=self._on_rainbow_change ) - + # ═══════════════════════════════════════════════════ # DESIGN SECTION # ═══════════════════════════════════════════════════ - + tw( parent=self._root_widget, position=(108.3, 301.9), @@ -785,10 +787,10 @@ class SettingsWindow: color=(0.75, 0.75, 0.75), scale=1.35 ) - + # Design buttons (3 styles) self._design_btns = [] - + # Box style btn1 = bw( parent=self._root_widget, @@ -800,7 +802,7 @@ class SettingsWindow: on_activate_call=lambda: self._set_style('box') ) self._design_btns.append(btn1) - + # Simple style btn2 = bw( parent=self._root_widget, @@ -812,7 +814,7 @@ class SettingsWindow: on_activate_call=lambda: self._set_style('simple') ) self._design_btns.append(btn2) - + # Minimal style btn3 = bw( parent=self._root_widget, @@ -824,18 +826,18 @@ class SettingsWindow: on_activate_call=lambda: self._set_style('minimal') ) self._design_btns.append(btn3) - + # ═══════════════════════════════════════════════════ # RESULT SECTION (Preview) # ═══════════════════════════════════════════════════ - + result_container = cw( parent=self._root_widget, position=(1147.2, 121.9), size=(450.0, 500.0), color=(0.25, 0.25, 0.25) ) - + tw( parent=self._root_widget, position=(1328.3, 581.9), @@ -844,7 +846,7 @@ class SettingsWindow: color=(0.75, 0.75, 0.75), scale=1.55 ) - + # Preview text widget self._preview_text = tw( parent=result_container, @@ -857,22 +859,22 @@ class SettingsWindow: scale=0.5, maxwidth=400 ) - + # Start preview update loop self._update_preview() - + cw(edit=self._root_widget, on_cancel_call=self._close) - + def _generate_preview_text(self): """Generate preview text based on current settings""" player_count = 3 # Sample count - + # Get icon icon_text = f"{PLAYERS_ICON} " if PLAYERS_ICON else "" - + """Generate preview text based on current settings""" player_count = 3 # Sample count - + if self._temp_style == 'box': header = "╔═══════════════════╗\n" header += f"║ PLAYERS ({player_count}) ║\n" @@ -886,11 +888,11 @@ class SettingsWindow: else: # minimal header = f" PLAYERS ({player_count})\n\n" footer = "" - + # Sample names player_name = "YT!Drap" account_name = "Drap" - + # Build player line based on settings if self._temp_show_full: # Full name: show both @@ -901,69 +903,69 @@ class SettingsWindow: name_part = account_name else: name_part = player_name - + # Add client ID if enabled if self._temp_show_id: player_line = f" • {name_part} [#42]" else: player_line = f" • {name_part}" - + return header + player_line + footer - + def _update_preview(self): """Update preview with rainbow effect if enabled""" if not hasattr(self, '_preview_text') or not self._preview_text.exists(): return - + # Update preview text tw(edit=self._preview_text, text=self._generate_preview_text()) - + # Update color (rainbow or static) if self._temp_rainbow: # Smooth rainbow loop if not hasattr(self, '_rainbow_hue'): self._rainbow_hue = 0.0 - + self._rainbow_hue = (self._rainbow_hue + 0.02) % 1.0 color = hsv_to_rgb(self._rainbow_hue, 1.0, 1.0) tw(edit=self._preview_text, color=color) else: tw(edit=self._preview_text, color=self._temp_color) - + # Schedule next update babase.apptimer(0.05, babase.WeakCall(self._update_preview)) - + def _set_name_type(self, name_type: str): """Set name type""" self._temp_name_type = name_type - + def _set_style(self, style: str): """Set design style and update button colors""" self._temp_style = style - + # Update all design buttons for idx, btn in enumerate(self._design_btns): if self.DESIGNS[idx] == style: bw(edit=btn, color=(0.1, 1.2, 0.1)) else: bw(edit=btn, color=(0.75, 0.85, 0.75)) - + def _on_full_name_change(self, value: bool): self._temp_show_full = value - + def _on_client_id_change(self, value: bool): self._temp_show_id = value - + def _on_rainbow_change(self, value: bool): self._temp_rainbow = value - + def _on_color_selected(self, color: tuple): self._temp_color = color bw(edit=self._color_display, color=color) - + def _open_color_picker(self): ColorPickerWindow(self._on_color_selected, self) - + def _apply_changes(self): """Apply all changes and save to config""" cfg = babase.app.config @@ -974,53 +976,53 @@ class SettingsWindow: cfg['Players Display - Show Client ID'] = self._temp_show_id cfg['Players Display - Style'] = self._temp_style cfg.commit() - + bui.screenmessage('Settings Applied!', color=(0, 1, 0)) bui.getsound('cashRegister').play() - + def _close(self): cw(edit=self._root_widget, transition='out_scale') class PlayersDisplay: """Main display class""" - + def __init__(self): self.container = None self.rainbow_hue = 0.0 self.current_players = {} # Track: {client_id: (player_name, account_name)} - + def on_app_running(self): self._update_loop() - + def _update_loop(self): self._update_display() - + # Check if rainbow mode is enabled cfg = babase.app.config is_rainbow = cfg.get('Players Display - Rainbow', False) - + # Use faster loop for smooth rainbow, slower for normal updates delay = 0.05 if is_rainbow else 0.5 babase.apptimer(delay, babase.WeakCall(self._update_loop)) - + def _get_display_color(self): cfg = babase.app.config - + if cfg.get('Players Display - Rainbow', False): # Smooth rainbow loop with smaller increment self.rainbow_hue = (self.rainbow_hue + 0.01) % 1.0 return hsv_to_rgb(self.rainbow_hue, 1.0, 1.0) else: return cfg.get('Players Display - Color', (0.7, 0.7, 0.7)) - + def _show_message(self, message: str, color: tuple, duration: float = 1.0): """Show a temporary message with fade in/out animation""" try: activity = bs.get_foreground_host_activity() if activity is None: return - + with activity.context: # Create message node msg_node = bs.newnode( @@ -1039,32 +1041,32 @@ class PlayersDisplay: 'opacity': 0.0 # Start invisible } ) - + # Fade in animation (0.2 seconds) bs.animate(msg_node, 'opacity', { 0.0: 0.0, 0.2: 0.95 }) - + # Fade out animation (starts after duration) bs.animate(msg_node, 'opacity', { duration: 0.95, duration + 0.3: 0.0 }) - + # Delete node after animations complete bs.timer(duration + 0.4, msg_node.delete) - + except Exception: pass - + def _check_player_changes(self, new_players: dict): """Check for players joining or leaving new_players: {client_id: (player_name, account_name)} """ new_ids = set(new_players.keys()) old_ids = set(self.current_players.keys()) - + # Players who joined joined = new_ids - old_ids for player_id in joined: @@ -1072,7 +1074,7 @@ class PlayersDisplay: message = f"{account_name} joined the server" color = self._get_display_color() self._show_message(message, color, duration=1.0) - + # Players who left left = old_ids - new_ids for player_id in left: @@ -1080,16 +1082,16 @@ class PlayersDisplay: message = f"{player_name} left the server" color = (1.2, 0.2, 0.2) # Glowing red self._show_message(message, color, duration=1.0) - + # Update current players self.current_players = new_players.copy() - + def _update_display(self): try: activity = bs.get_foreground_host_activity() except Exception: activity = None - + if activity is None: if self.container is not None: try: @@ -1098,37 +1100,37 @@ class PlayersDisplay: pass self.container = None return - + cfg = babase.app.config show_full = cfg.get('Players Display - Show Full Name', False) name_type = cfg.get('Players Display - Name Type', 'Player Name') show_client_id = cfg.get('Players Display - Show Client ID', True) style = cfg.get('Players Display - Style', 'box') - + player_names = [] new_players_data = {} # {client_id: (player_name, account_name)} - + for p in activity.players: try: # Get both player name and account name player_name = p.getname(full=True) - + try: account_name = p.sessionplayer.inputdevice.get_v1_account_name(full=True) if not account_name: account_name = player_name except Exception: account_name = player_name - + # Get client ID try: client_id = p.sessionplayer.inputdevice.client_id except Exception: client_id = id(p) # Fallback to object id - + # Store for tracking new_players_data[client_id] = (player_name, account_name) - + # Build name based on settings if show_full: # Full name: show both names @@ -1142,7 +1144,7 @@ class PlayersDisplay: name = account_name else: name = player_name - + # Add client ID if enabled if show_client_id: try: @@ -1151,19 +1153,19 @@ class PlayersDisplay: player_names.append(f" • {name}") else: player_names.append(f" • {name}") - + except Exception: continue - + # Check for player changes (join/leave) self._check_player_changes(new_players_data) - + # Get player count player_count = len(player_names) - + # Get icon icon_text = f"{PLAYERS_ICON} " if PLAYERS_ICON else "" - + # Different styles with player count if style == 'box': header = "╔═══════════════════╗\n" @@ -1178,15 +1180,15 @@ class PlayersDisplay: else: # minimal header = f" PLAYERS ({player_count})\n\n" footer = "" - + if player_names: players_str = "\n".join(player_names) else: players_str = " ⏳ Waiting...\n No players yet" - + final_text = header + players_str + footer current_color = self._get_display_color() - + try: with activity.context: if self.container is None or not self.container.exists(): @@ -1214,12 +1216,14 @@ class PlayersDisplay: # My First Cool Mod ,WooHoo. # ba_meta export babase.Plugin + + class CoolList(babase.Plugin): """Main plugin class""" - + def __init__(self): cfg = babase.app.config - + if 'Players Display - Show Full Name' not in cfg: cfg['Players Display - Show Full Name'] = False # Default to single name if 'Players Display - Name Type' not in cfg: @@ -1232,14 +1236,14 @@ class CoolList(babase.Plugin): cfg['Players Display - Show Client ID'] = True if 'Players Display - Style' not in cfg: cfg['Players Display - Style'] = 'box' - + cfg.commit() - + self.display = PlayersDisplay() self.display.on_app_running() - + def has_settings_ui(self) -> bool: return True - + def show_settings_ui(self, source_widget: Any) -> None: SettingsWindow(source_widget) From c7c054013427d27f6a1f91596fbdebe9dfa4d90d Mon Sep 17 00:00:00 2001 From: DrapBS <260050423+DrapBS@users.noreply.github.com> Date: Sat, 14 Feb 2026 09:07:31 +0000 Subject: [PATCH 12/84] [ci] apply-version-metadata --- plugins/utilities.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index e13632d..0c62bac 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2419,7 +2419,12 @@ } ], "versions": { - "1.7.0": null + "1.7.0": { + "api_version": 9, + "commit_sha": "0d39f57", + "released_on": "14-02-2026", + "md5sum": "d8002dfc93be74d71f8e6e4aacfe6fd1" + } } } } From af881afd9b8341807e1c27de274be088354f5c29 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:06:59 +0530 Subject: [PATCH 13/84] added plugin bug report form template --- .github/ISSUE_TEMPLATE/plugin-bug-report.md | 46 ---------- .github/ISSUE_TEMPLATE/plugin-bug-report.yml | 89 ++++++++++++++++++++ CHANGELOG.md | 15 ++-- index.json | 1 + plugin_manager.py | 48 ++++++++--- 5 files changed, 136 insertions(+), 63 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/plugin-bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/plugin-bug-report.yml diff --git a/.github/ISSUE_TEMPLATE/plugin-bug-report.md b/.github/ISSUE_TEMPLATE/plugin-bug-report.md deleted file mode 100644 index 40c76c0..0000000 --- a/.github/ISSUE_TEMPLATE/plugin-bug-report.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -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/ISSUE_TEMPLATE/plugin-bug-report.yml b/.github/ISSUE_TEMPLATE/plugin-bug-report.yml new file mode 100644 index 0000000..6d1f5bc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/plugin-bug-report.yml @@ -0,0 +1,89 @@ +name: Plugin Bug Report +description: Create a report about a plugin to help us improve. +title: '[PLUGIN BUG]: ' +labels: ['Plugin Bug'] + +body: + - type: input + id: plugin-name + attributes: + label: Plugin Name + description: The name of the plugin experiencing the bug. + placeholder: e.g. colorscheme + validations: + required: true + + - type: textarea + id: bug-description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + id: console-log + attributes: + label: Console Log + description: Goto settings -> advanced -> Dev Tools -> enable "Show dev console button" and reproduce the bug. Then copy and paste the console log here. + + - type: textarea + id: reproduction-steps + attributes: + label: To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + + - type: input + id: os-version + attributes: + label: OS with Version + placeholder: e.g. Windows 11 / Android 13 + + - type: input + id: bombsquad-version + attributes: + label: BombSquad Version + placeholder: e.g. v1.7.60 + validations: + required: true + + - type: input + id: plugin-manager-version + attributes: + label: Plugin Manager Version + placeholder: e.g. v1.1.10 + validations: + required: true + + - type: input + id: plugin-version + attributes: + label: Plugin Version + placeholder: e.g. 2.0.1 + validations: + required: true + + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Drag and drop screenshots here to help explain the problem. + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context about the problem here (logs, other plugins installed, etc). diff --git a/CHANGELOG.md b/CHANGELOG.md index 031ef8f..3603c84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Plugin Manager (dd-mm-yyyy) +### 1.1.7 (15-02-2026) + +- Added function to fill form details for plugin bug report button +- Added a new plugin bug report yaml template + ### 1.1.6 (26-01-2026) - Fix UI scaling for more window @@ -31,15 +36,15 @@ ### 1.1.1 (09-02-2025) - Added bomb spinner widget for loading animation. - + ### 1.1.0 (23-01-2025) -- Updated to bombsquad api 9. - +- 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. @@ -193,13 +198,11 @@ - 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. diff --git a/index.json b/index.json index 2af4f5f..f3c199c 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,7 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { + "1.1.7": null, "1.1.6": { "api_version": 9, "commit_sha": "b0c463e", diff --git a/plugin_manager.py b/plugin_manager.py index 8bb3006..7e5b8a2 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -26,7 +26,7 @@ from datetime import datetime # Modules used for overriding AllSettingsWindow import logging -PLUGIN_MANAGER_VERSION = "1.1.6" +PLUGIN_MANAGER_VERSION = "1.1.7" 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. @@ -35,8 +35,8 @@ CURRENT_TAG = "main" _env = babase.env() _app_api_version = babase.app.env.api_version -PLUGIN_ISSUE_TEMPLATE = "plugin-bug-report.md" -GITHUB_ISSUE_META = "{repository_url}/issues/new?template={issue_template}" +PLUGIN_ISSUE_TEMPLATE = "plugin-bug-report.yml" +GITHUB_PLUGIN_ISSUE_META = "{repository_url}/issues/new?template={issue_template}" INDEX_META = "{repository_url}/{content_type}/{tag}/index.json" CHANGELOG_META = "{repository_url}/{content_type}/{tag}/CHANGELOG.md" HEADERS = { @@ -1798,8 +1798,10 @@ class PluginWindow(popup.PopupWindow): class MoreWindow: - def __init__(self, plugin, origin=None): + def __init__(self, plugin: Plugin, origin=None): _add_popup(self) + + self.plugin = plugin # collect info last_updated = None for value in plugin.info['versions'].values(): @@ -1912,13 +1914,7 @@ class MoreWindow: button_type='square', color=(0.6, 0.53, 0.63), label='', - on_activate_call=bui.CallPartial( - bui.open_url, - GITHUB_ISSUE_META.format( - repository_url=REPOSITORY_URL, - issue_template=PLUGIN_ISSUE_TEMPLATE - ) - ) + on_activate_call=self._open_bug_report_url ) bui.imagewidget( parent=self._root_widget, @@ -1985,6 +1981,36 @@ class MoreWindow: down_widget=source_button ) + def _open_bug_report_url(self): + import platform + import urllib.parse + import baenv + + LOGS_LENGTH_LIMIT = 5000 + _logs = "" + for entry in baenv._EnvGlobals.get().config.log_handler.get_cached().entries: + _logs += entry.message + "\n" + + # reduce this more if it's too long + error_logs = f"""```py +{_logs[:LOGS_LENGTH_LIMIT]} +```""" + + params = { + "title": "[PLUGIN BUG]: " + self.plugin.name, + "plugin-name": self.plugin.name, + "plugin-version": self.plugin.create_local().version if self.plugin.is_installed else 'Not Installed', + "plugin-manager-version": 'v' + PLUGIN_MANAGER_VERSION, + "bombsquad-version": 'v' + babase.app.env.engine_version + " (" + str(babase.app.env.engine_build_number) + ")", + "os-version": platform.platform() if platform else babase.app.env.platform.value + babase.app.env.os_version, + "console-log": error_logs + } + query_string = urllib.parse.urlencode(params, quote_via=urllib.parse.quote) + base_url = GITHUB_PLUGIN_ISSUE_META.format( + repository_url=REPOSITORY_URL, issue_template=PLUGIN_ISSUE_TEMPLATE) + final_url = f"{base_url}&{query_string}" + bui.open_url(final_url) + def _back(self) -> None: _remove_popup(self) bui.getsound('swish').play() From 13a86bec22c8b4283ce785d0586c5e10d5431a47 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Sun, 15 Feb 2026 18:32:23 +0000 Subject: [PATCH 14/84] [ci] apply-version-metadata --- index.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.json b/index.json index f3c199c..3ee912d 100644 --- a/index.json +++ b/index.json @@ -1,7 +1,12 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { - "1.1.7": null, + "1.1.7": { + "api_version": 9, + "commit_sha": "af881af", + "released_on": "15-02-2026", + "md5sum": "454cbacd47c813154f2f9cb0d2736067" + }, "1.1.6": { "api_version": 9, "commit_sha": "b0c463e", From 455967d4fd0ffea326df58995b083c3002afd648 Mon Sep 17 00:00:00 2001 From: ! Freaku <92618708+Freaku17@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:34:56 +0530 Subject: [PATCH 15/84] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index c1cc192..46ade36 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Members of bombsquad-community organization, and contributors +Copyright (c) 2022-2026 Members of bombsquad-community organization, and contributors All source code in this repository is licensed under MIT, unless otherwise explicitly stated in source code. From d07385de2d276f342e16fcafb34453150674b8a6 Mon Sep 17 00:00:00 2001 From: ! Freaku <92618708+Freaku17@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:38:39 +0530 Subject: [PATCH 16/84] Update README Regarding installation of plugin manager --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 963ab1b..b78af51 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,13 @@ which makes further modding of your game more convenient by providing easier acc ## Installation -There are two different ways the plugin manager can be installed: +There are three different ways the plugin manager can be installed: 1. From dev console - Enable "Show Dev Console Button" from advance BombSquad settings + - Make sure you're connected to the internet - 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") @@ -57,7 +58,7 @@ There are two different ways the plugin manager can be installed: 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). + Settings -> Advanced -> Show Mods Folder). 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/): From ebbd1986280510e1afc2b2315e5d4523a73c3b24 Mon Sep 17 00:00:00 2001 From: DinoWattz <116862698+DinoWattz@users.noreply.github.com> Date: Mon, 23 Feb 2026 05:16:06 -0300 Subject: [PATCH 17/84] Upload plugins --- plugins/utilities/better_camera_shake.py | 381 +++++++++++++++ plugins/utilities/chat_bubbles.py | 582 +++++++++++++++++++++++ plugins/utilities/enhanced_effects.py | 290 +++++++++++ plugins/utilities/sleep_on_afk.py | 116 +++++ 4 files changed, 1369 insertions(+) create mode 100644 plugins/utilities/better_camera_shake.py create mode 100644 plugins/utilities/chat_bubbles.py create mode 100644 plugins/utilities/enhanced_effects.py create mode 100644 plugins/utilities/sleep_on_afk.py diff --git a/plugins/utilities/better_camera_shake.py b/plugins/utilities/better_camera_shake.py new file mode 100644 index 0000000..634fb1a --- /dev/null +++ b/plugins/utilities/better_camera_shake.py @@ -0,0 +1,381 @@ +# ba_meta require api 9 +from __future__ import annotations + +import babase +import bascenev1 as bs +import bascenev1lib.actor.bomb as bomb + +import random +from typing import TYPE_CHECKING + +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.bomb import BombFactory, ExplodeHitMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz + +if TYPE_CHECKING: + from typing import Any, Sequence + +plugman = dict( + plugin_name="better_camera_shake", + description="This plugin makes the camera only shake when an explosion hits a player character, begone excessive camera shaking!", + external_url="", + authors=[ + {"name": "DinoWattz", "email": "", "discord": ""} + ], + version="1.0.0", +) + +# Camera shake only on player impact +def new__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', + ): + """Instantiate with given values.""" + + # bah; get off my lawn! + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + + super(type(self), self).__init__() + + 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) + + # ↓ We don't need any of this, we're fully trained professionals. + #bs.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.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) + +bomb.Blast.__init__ = new__init__ + +org_handlemessage = bomb.Blast.handlemessage +def new_handlemessage(self, msg: Any, *args, **kwargs) -> Any: + org_handlemessage(self, msg, *args, **kwargs) + + if isinstance(msg, ExplodeHitMessage): + node = bs.getcollision().opposingnode + delegate = node.getdelegate(PlayerSpaz) + has_used_camerashake = getattr(self, 'has_used_camerashake', False) + if not has_used_camerashake and (delegate and not delegate.shield) and not node.invincible: + self.has_used_camerashake = True + bs.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0) + + +bomb.Blast.handlemessage = new_handlemessage + +# ba_meta export babase.Plugin +class Plugin(babase.Plugin): + pass \ No newline at end of file diff --git a/plugins/utilities/chat_bubbles.py b/plugins/utilities/chat_bubbles.py new file mode 100644 index 0000000..446ad07 --- /dev/null +++ b/plugins/utilities/chat_bubbles.py @@ -0,0 +1,582 @@ +# ba_meta require api 9 +# This plugin only works with BombSquad 1.7.49+ +from __future__ import annotations + +from typing import TYPE_CHECKING, override + +import babase +import bascenev1 as bs +import random +import string +import unicodedata + +if TYPE_CHECKING: + from typing import Any + +plugman = dict( + plugin_name="chat_bubbles", + description="Adds whatever the players say above their character, includes a few other features too!", + external_url="", + authors=[ + {"name": "DinoWattz", "email": "", "discord": ""} + ], + version="1.0.0", +) + +PRINT_CHAT_MESSAGES = True # Prints chat messages to the console as "[Character's Name] Player 1/Player 2: Hello World!" +BUBBLE_DURATION: float = 4.7 # 4.7 matches the default onscreen message duration +SPEAK_VOLUME = 1.5 +SHOUT_VOLUME = 3.0 + +CELEBRATE_MESSAGE = [(bs.CelebrateMessage, {"duration": 1.0})] + +# Easter egg triggers, sounds, and messages flag +# If adding new ones, make sure to test them in-game +EASTER_EGGS = [ + { + "triggers": { + "hi", "hello", "hey", "hiya", "yo", "sup", "heya", "howdy", + "greetings", "salutations", "ahoy", "ahoyhoy", "bonjour", + "hola", "ciao", "ola", "oi", "eae", "salve", + "bye", "goodbye", "see ya", "see you", "cya", + "adios", "chau", "chao", "tchau", + "adeus", "ate mais", "falou", "flw" + }, + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"aaa"}, + "sound": "spazFall01", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"aaaa"}, + "sound": "zoeFall01", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"aaaaa"}, + "sound": "kronkFall", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"potato"}, + "sound": "kronk2", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"dau", "tau", "d'oh", "doh"}, + "sound": "kronk3", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"drau", "trau", "d'roh", "droh"}, + "sound": "bunny2", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"hahaha", "hahahaha", + "hahahah", "hahahahah", "lol", + "nahahah", "kakaka", "kakakaka", "kkk", "kkkk", + "jajaja", "jajajaja", "jajajajaa"}, + "sound": "mel05", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"ha", "hah", "ja", "ka"}, + "sound": "mel06", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"merry christmas", "merry krismas", "merry xmas", "feliz natal", "feliz navidad"}, + "sound": "santa02", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"hohoho", "ho ho ho", "ho-ho-ho"}, + "sound": "santa05", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"gg", "good game", "que pro", "q pro"}, + "sound": "achievement", + "messages": CELEBRATE_MESSAGE, + }, + { + "triggers": {"boo", "bad game", "que noob", "q noob"}, + "sound": "boo", + "messages": CELEBRATE_MESSAGE, + }, + # ↓ Node messages are like this ↓ + { + "triggers": {"0", "zero"}, + "sound": "boxingBell", + "node_messages": [("celebrate_l", 1000)], + }, + { + "triggers": {"1", "one"}, + "sound": "announceOne", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"2", "two"}, + "sound": "announceTwo", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"3", "three"}, + "sound": "announceThree", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"4", "four"}, + "sound": "announceFour", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"5", "five"}, + "sound": "announceFive", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"6", "six"}, + "sound": "announceSix", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"7", "seven"}, + "sound": "announceSeven", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"8", "eight"}, + "sound": "announceEight", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"9", "nine"}, + "sound": "announceNine", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"10", "ten"}, + "sound": "announceTen", + "node_messages": [("celebrate_r", 1000)], + }, + { + "triggers": {"xd"}, + "sound": "santaDeath", + "node_messages": [("knockout", 100)], + }, + # ↓ Easter eggs without actor/node messages ↓ + { + "triggers": {"huh", "huhh", "huhhh", "huhhhh", "humph", "hmph", "hum", "humm"}, + "sound": "agent1", + }, + { + "triggers": {"hoh", "hohh", "hohhh", "hohhhh", "ohh"}, + "sound": "agent3", + }, + { + "triggers": {"eff"}, + "sound": "zoeEff", + }, + { + "triggers": {"ow", "oww", "oow"}, + "sound": "zoeOw", + }, + { + "triggers": {"pipipi", "bibibi"}, + "sound": "mel07", + }, + { + "triggers": {"oh yeah", "oh yea", "oohh yeah", "oohh yea"}, + "sound": "yeah", + }, + { + "triggers": {"woo"}, + "sound": "woo2", + }, + { + "triggers": {"wooo"}, + "sound": "woo", + }, + { + "triggers": {"woo yeah", "woo yea"}, + "sound": "woo3", + }, + { + "triggers": {"ooh"}, + "sound": "ooh", + }, + { + "triggers": {"wow"}, + "sound": "wow", + }, + { + "triggers": {"gasp"}, + "sound": "gasp", + }, + { + "triggers": {"ahh", "aah"}, + "sound": "aww", + }, + { + "triggers": {"nice"}, + "sound": "nice", + }, + # ↑ Default easter egg list ends here ↑ +] + +# Chat bubble functionality +def create_bubble(player: bs.SessionPlayer, text: str, name: str): + msg = normalize_message(text) + is_caps = len(text) > 1 and text.isupper() + handled = False + bubble_created = False + + activityplayer: bs.Player | None = getattr(player, 'activityplayer', None) + + def _fallback_logic(): + nonlocal handled, activityplayer + color = (1, 1, 0.4) + image_icon = {'texture': bs.gettexture("cuteSpaz"), 'tint_texture': bs.gettexture("cuteSpaz"), 'tint_color': (1.0, 1.0, 1.0), 'tint2_color': (1.0, 1.0, 1.0)} + + if activityplayer: + normal_highlight = bs.normalized_color( bs.safecolor(player.highlight, target_intensity=1.15) ) + saturated_highlight = bs.normalized_color( bs.safecolor(player.highlight, target_intensity=0.75) ) + color = saturated_highlight if is_caps else normal_highlight + image_icon = activityplayer.get_icon() + + bs.broadcastmessage(f"{name}: {text}", + color=color, + top=True, + image=image_icon) + + for egg in EASTER_EGGS: + if msg in egg["triggers"]: + if egg.get("sound"): + sound = bs.getsound(egg["sound"]) + sound.play(SHOUT_VOLUME if is_caps else SPEAK_VOLUME) + handled = True + break + if not handled and activityplayer: + assert bs.app.classic is not None + handled = True + character = activityplayer.character + char = bs.app.classic.spaz_appearances[character] + if is_caps: + attack_sounds = [bs.getsound(s) for s in char.attack_sounds] + if attack_sounds and isinstance(attack_sounds, (list, tuple)) and len(attack_sounds) > 0: + sound = random.choice(attack_sounds) + sound.play(SHOUT_VOLUME) + else: + pickup_sounds = [bs.getsound(s) for s in char.pickup_sounds] + if pickup_sounds and isinstance(pickup_sounds, (list, tuple)) and len(pickup_sounds) > 0: + sound = random.choice(pickup_sounds) + sound.play(SPEAK_VOLUME) + + if activityplayer is None: + # No valid activityplayer: play sound if possible (lobby/non-in-game) + _fallback_logic() + return + + bubbles = activityplayer.customdata.setdefault('chat_bubbles', []) + actor_list = [] + + # Support for both actor and ghost_actor (from the Ghost Players plugin) + if getattr(activityplayer, 'actor', None): + actor_list.append(activityplayer.actor) + if hasattr(activityplayer, 'customdata') and activityplayer.customdata.get('ghost_actor'): + actor_list.append(activityplayer.customdata['ghost_actor']) + + if actor_list: + for actor in actor_list: + if actor and actor.is_alive() and actor.node: + assert actor is not None + char_node = actor.node + activity = actor.getactivity() + if activity is None or getattr(activity, 'expired', False): + continue + + with activity.context: + # Calculate scale based on text length + base_scale = 0.015 + min_scale = 0.009 + scale = max(base_scale - 0.0003 * max(len(text) - 20, 0), min_scale) + y_offset = 1.2 + 0.25 * len(bubbles) + + mnode = bs.newnode('math', owner=char_node, attrs={ + 'input1': (0, y_offset, 0), + 'operation': 'add' + }) + char_node.connectattr('torso_position', mnode, 'input2') + + normal_highlight = bs.normalized_color( bs.safecolor(player.highlight, target_intensity=1.15) ) + saturated_highlight = bs.normalized_color( bs.safecolor(player.highlight, target_intensity=0.75) ) + bubble_color = saturated_highlight if is_caps else normal_highlight + + textnode = bs.newnode('text', + owner=char_node, + attrs={ + 'text': text, + 'color': bubble_color, + 'shadow': 0.5, + 'flatness': 0.5, + 'scale': scale, + 'h_align': 'center', + 'v_align': 'bottom', + 'in_world': True, + 'opacity': 1.0, + }) + + mnode.connectattr('output', textnode, 'position') + + focus = Plugin.Focus(owner=textnode).autoretain() + textnode.connectattr('position', focus.node, 'position') + + # Adjust bubble duration for slow-motion activities + bubble_duration = BUBBLE_DURATION + if hasattr(activity, 'slow_motion') and activity.slow_motion: + bubble_duration = max(0.5, bubble_duration / 3) + + # Play sound for all triggers (easter eggs and normal) + def _play_chat_sound(actor, sound, volume): + if actor.is_alive(): + actor._safe_play_sound(sound, volume) + else: + sound.play(volume) + + # Easter egg sound/message + for egg in EASTER_EGGS: + if msg in egg['triggers']: + # Sound selection logic + sound = None + if egg.get('sound'): + sound = bs.getsound(egg['sound']) + else: + # Default to character sounds if no specific sound is set + if is_caps: + attack_sounds = getattr(char_node, 'attack_sounds', None) + if attack_sounds and isinstance(attack_sounds, (list, tuple)) and len(attack_sounds) > 0: + sound = random.choice(attack_sounds) + else: + pickup_sounds = getattr(char_node, 'pickup_sounds', None) + if pickup_sounds and isinstance(pickup_sounds, (list, tuple)) and len(pickup_sounds) > 0: + sound = random.choice(pickup_sounds) + + if sound: + _play_chat_sound(actor, sound, SHOUT_VOLUME if is_caps else SPEAK_VOLUME) + + # Animation logic + if egg.get('messages') and actor.is_alive() and hasattr(actor, 'handlemessage'): + for msg_class, msg_kwargs in egg.get('messages', []): + msg_kwargs = msg_kwargs or {} + actor.handlemessage(msg_class(**msg_kwargs)) + + if egg.get('node_messages') and actor.is_alive() and actor.node.exists(): + for node_args in egg.get('node_messages', []): + actor.node.handlemessage(*node_args) + + handled = True + break + + if not handled: + if is_caps: + attack_sounds = getattr(char_node, 'attack_sounds', None) + if attack_sounds and isinstance(attack_sounds, (list, tuple)) and len(attack_sounds) > 0: + sound = random.choice(attack_sounds) + _play_chat_sound(actor, sound, SHOUT_VOLUME) + if actor.is_alive() and hasattr(actor, 'handlemessage'): + actor.handlemessage(bs.CelebrateMessage(duration=1.0)) + else: + pickup_sounds = getattr(char_node, 'pickup_sounds', None) + if pickup_sounds and isinstance(pickup_sounds, (list, tuple)) and len(pickup_sounds) > 0: + sound = random.choice(pickup_sounds) + _play_chat_sound(actor, sound, SPEAK_VOLUME) + + bubbles.append((textnode, mnode)) + bubble_created = True + while len(bubbles) > 3: + old_textnode, old_mnode = bubbles.pop(0) + if old_textnode is not None and hasattr(old_textnode, 'exists') and old_textnode.exists(): + old_textnode.delete() + if old_mnode is not None and hasattr(old_mnode, 'exists') and old_mnode.exists(): + old_mnode.delete() + update_bubble_offsets(bubbles) + + fade_time = max(0.1, bubble_duration - 0.5) + bs.animate(textnode, 'opacity', {fade_time: 1.0, bubble_duration: 0.0}) + bs.timer(bubble_duration, textnode.delete) + bs.timer(bubble_duration, mnode.delete) + + def cleanup(): + if hasattr(activityplayer, 'customdata'): + if 'chat_bubbles' in activityplayer.customdata: + activityplayer.customdata['chat_bubbles'] = [ + b for b in activityplayer.customdata['chat_bubbles'] if b[0] != textnode + ] + bs.timer(bubble_duration, cleanup) + + if not bubble_created: + # No valid actor found: play sound if possible + _fallback_logic() + return + +def update_bubble_offsets(bubbles): + for i, (tnode, mnode) in enumerate(reversed(bubbles)): + y_offset = 1.2 + 0.25 * i + if mnode is not None and hasattr(mnode, 'exists') and mnode.exists(): + mnode.input1 = (0, y_offset, 0) + +# Tools +def normalize_message(text): + msg = text.strip().lower() + msg = msg.translate(str.maketrans('', '', string.punctuation)) + msg = ''.join( + c for c in unicodedata.normalize('NFD', msg) + if unicodedata.category(c) != 'Mn' + ) + + return msg + +# ba_meta export babase.Plugin +class Plugin(babase.Plugin): + def on_app_running(self) -> None: + # Hook into chat message filtering + # We're delaying until the app runs and using a timer so it's more likely to work with other chat plugins (please don't make a chat plugin/filter that is delayed like this). + bs.apptimer(0.001, bs.WeakCallStrict(self.apply_patch)) + + def apply_patch(self): + import bascenev1._hooks as _hooks + + _org_filter_chat_message = _hooks.filter_chat_message + def _chat_bubbles_hook(msg: str, client_id: int, *args, **kwargs) -> str | None: + org_msg = _org_filter_chat_message(msg, client_id, *args, **kwargs) + + if org_msg is None: + # The original message has been ignored, so we should ignore it as well + return org_msg + + # Chat bubble sorcery + try: + roster = bs.get_game_roster() + session = bs.get_foreground_host_session() + if session is None: + # Probably couldn't find a session because we are a client + return org_msg + sessionplayers = session.sessionplayers + + player_list = [] + matched = False + dead_bubble = False + character: str | None = None + + with session.context: + for player in sessionplayers: + if player.inputdevice.client_id == client_id and player.in_game: + player_list.append(player) + continue + if player_list: + # Get combined name for multiple players on same client + name = '' + created_bubble = False + + for player in player_list: + if name == '': + name = player.getname() + else: + name = name + '/' + player.getname() + + # Create chat bubble + for player in player_list: + activityplayer: bs.Player = player.activityplayer + # We have a session player but not an activity player + if not activityplayer: + if not dead_bubble: + create_bubble(player, org_msg, name) + created_bubble = dead_bubble = True + continue + # We have a dead activity player + if not activityplayer.is_alive() and not dead_bubble: + create_bubble(player, org_msg, name) + created_bubble = dead_bubble = True + character = activityplayer.character if character is None else character + # elif we have an alive activity player + elif activityplayer.is_alive(): + create_bubble(player, org_msg, name) + created_bubble = True + character = activityplayer.character if character is None else character + + if created_bubble: + matched = True + if PRINT_CHAT_MESSAGES: + print(f"[{character}] {name}: {org_msg}") + else: + bs.logging.warning("Failed to create a chat bubble, using non-session player method now.") + # Non-session players (in lobby or just spectating, this only works for servers) + if not matched and not dead_bubble: + for client in roster: + if client['client_id'] == client_id: + name = client.get('display_string') + create_bubble(None, org_msg, name) + + matched = True + if PRINT_CHAT_MESSAGES: + print(f"[{character}] {name}: {org_msg}") + break + except Exception as e: + bs.logging.exception(f"Error in chat bubble hook: {e}") + + return org_msg + + _hooks.filter_chat_message = _chat_bubbles_hook + bs.reload_hooks() + + print("[ChatBubbles] Plugin initialized.") + + class Focus(bs.Actor): + def __init__( + self, + *, + owner: bs.Node | None = None, + ): + super().__init__() + + self._focusnode_material = bs.Material() + self._focusnode_material.add_actions( + actions=('modify_node_collision', 'collide', False), + ) + + if getattr(owner, 'owner', None): + assert owner is not None + area_of_interest = False if getattr(owner.owner, 'is_area_of_interest', False) else True + else: + area_of_interest = False if getattr(owner, 'is_area_of_interest', False) else True + + # I'm tired of this area_of_interest nonsense, it never works right + #area_of_interest = False + # actually it might work now that we don't spawn bubbles for dead players. + + self.node = bs.newnode( + 'prop', + delegate=self, + owner=owner, + attrs={ + 'body': 'sphere', + 'body_scale': 0.0, + 'shadow_size': 0.0, + 'gravity_scale': 0.0, + 'is_area_of_interest': area_of_interest, + 'materials': (self._focusnode_material,), + }, + ) + + @override + 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) diff --git a/plugins/utilities/enhanced_effects.py b/plugins/utilities/enhanced_effects.py new file mode 100644 index 0000000..9a33534 --- /dev/null +++ b/plugins/utilities/enhanced_effects.py @@ -0,0 +1,290 @@ +# ba_meta require api 9 +from __future__ import annotations + +from typing import TYPE_CHECKING + +import random +import weakref +import babase +import bascenev1 as bs + +from bascenev1lib.actor.background import Background + +if TYPE_CHECKING: + from typing import Any + +plugman = dict( + plugin_name="enhanced_effects", + description="Explosions affect character colors, slow-mo tnt, fair colored shields, a trippy background screen and maybe more in the future!", + external_url="", + authors=[ + {"name": "DinoWattz", "email": "", "discord": ""} + ], + version="1.0.0", +) + +# Transparent Background (kinda hacky, works best on pc with high/higher quality 'visuals' setting) +BACKGROUND_OPACITY = 0.5 +def __modified_background_init( + self, + fade_time: float = 0.5, + start_faded: bool = False, + show_logo: bool = False, + ): + super(type(self), self).__init__() + self._dying = False + self.fade_time = fade_time + # We're special in that we create our node in the session + # scene instead of the activity scene. + # This way we can overlap multiple activities for fades + # and whatnot. + session = bs.getsession() + self._session = weakref.ref(session) + with session.context: + self.node = bs.newnode( + 'image', + delegate=self, + attrs={ + 'fill_screen': True, + 'texture': bs.gettexture('bg'), + 'tilt_translate': -0.3, + 'has_alpha_channel': False, + 'color': (1, 1, 1), + 'opacity': BACKGROUND_OPACITY, + }, + ) + if not start_faded: + bs.animate( + self.node, + 'opacity', + {0.0: 0.0, self.fade_time: BACKGROUND_OPACITY}, + loop=False, + ) + if show_logo: + logo_texture = bs.gettexture('logo') + logo_mesh = bs.getmesh('logo') + logo_mesh_transparent = bs.getmesh('logoTransparent') + self.logo = bs.newnode( + 'image', + owner=self.node, + attrs={ + 'texture': logo_texture, + 'mesh_opaque': logo_mesh, + 'mesh_transparent': logo_mesh_transparent, + 'scale': (0.7, 0.7), + 'vr_depth': -250, + 'color': (0.15, 0.15, 0.15), + 'position': (0, 0), + 'tilt_translate': -0.05, + 'absolute_scale': False, + }, + ) + self.node.connectattr('opacity', self.logo, 'opacity') + # add jitter/pulse for a stop-motion-y look unless we're in VR + # in which case stillness is better + if not bs.app.env.vr: + self.cmb = bs.newnode( + 'combine', owner=self.node, attrs={'size': 2} + ) + for attr in ['input0', 'input1']: + bs.animate( + self.cmb, + attr, + {0.0: 0.693, 0.05: 0.7, 0.5: 0.693}, + loop=True, + ) + self.cmb.connectattr('output', self.logo, 'scale') + cmb = bs.newnode( + 'combine', owner=self.node, attrs={'size': 2} + ) + cmb.connectattr('output', self.logo, 'position') + # Gen some random keys for that stop-motion-y look. + keys = {} + timeval = 0.0 + for _i in range(10): + keys[timeval] = (random.random() - 0.5) * 0.0015 + timeval += random.random() * 0.1 + bs.animate(cmb, 'input0', keys, loop=True) + keys = {} + timeval = 0.0 + for _i in range(10): + keys[timeval] = (random.random() - 0.5) * 0.0015 + 0.05 + timeval += random.random() * 0.1 + bs.animate(cmb, 'input1', keys, loop=True) + +def _modified_background_die(self, immediate: bool = False) -> None: + session = self._session() + if session is None and self.node: + # If session is gone, our node should be too, + # since it was part of the session's scene. + # Let's make sure that's the case. + # (since otherwise we have no way to kill it) + bs.logging.exception( + 'got None session on Background _die' + ' (and node still exists!)' + ) + elif session is not None: + with session.context: + if not self._dying and self.node: + self._dying = True + if immediate: + self.node.delete() + else: + bs.animate( + self.node, + 'opacity', + {0.0: self.node.opacity, self.fade_time: 0.0}, + loop=False, + ) + bs.timer(self.fade_time + 0.1, self.node.delete) + +Background.__init__ = __modified_background_init +Background._die = _modified_background_die + +# Tools +def blend_toward( + rgb: tuple[float, float, float], + target: tuple[float, float, float], + amount: float = 1.0, + perceptual: bool = False +) -> tuple[float, ...]: + """ + Blend 'rgb' toward 'target' by 'amount' (0..1). + + If perceptual=True, blend in linear-light space for smoother, more + natural-looking fades. + """ + def clamp01(x: float) -> float: + return max(0.0, min(1.0, x)) + + def srgb_to_linear(c: float) -> float: + return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 + + def linear_to_srgb(c: float) -> float: + return 12.92 * c if c <= 0.0031308 else 1.055 * (c ** (1 / 2.4)) - 0.055 + + a = clamp01(amount) + + if not perceptual: + return tuple(clamp01(c + (t - c) * a) for c, t in zip(rgb, target)) + + # Gamma-aware blend + rgb_lin = [srgb_to_linear(clamp01(c)) for c in rgb] + target_lin = [srgb_to_linear(clamp01(c)) for c in target] + blended_lin = [ + c + (t - c) * a for c, t in zip(rgb_lin, target_lin) + ] + return tuple(clamp01(linear_to_srgb(c)) for c in blended_lin) + +# ba_meta export babase.Plugin +class Plugin(babase.Plugin): + def on_app_running(self) -> None: + from bascenev1lib.actor.bomb import Blast, ExplodeHitMessage + from bascenev1lib.actor.spaz import Spaz + from bascenev1lib.actor.spazbot import SpazBot + from bascenev1lib.actor.playerspaz import PlayerSpaz + + # Colored Shields + org_equip_shields = Spaz.equip_shields + + def equip_shields(self, decay: bool = False) -> None: + org_equip_shields(self, decay) + if self.shield is not None: + safe_highlight = bs.safecolor(getattr(self, '_original_highlight', self.node.highlight), target_intensity=0.75) + self.shield.color = bs.normalized_color(safe_highlight) + + Spaz.equip_shields = equip_shields + + # Slow Motion TNT Explosion + org_blast_init = Blast.__init__ + + def new_init(self: Blast, blast_type='normal', hit_type='explosion', *args, **kwargs) -> None: + org_blast_init(self, blast_type=blast_type, hit_type=hit_type, *args, **kwargs) + + if blast_type == 'tnt' and hit_type == 'explosion': + bs.camerashake(3.0) + activity = bs.getactivity() + gnode = activity.globalsnode + + # Pause + gnode.paused = True + self._pause_timer = bs.DisplayTimer(0.01, bs.CallPartial(setattr, gnode, 'paused', True), True) + + delay = 0.06 + + bs.displaytimer(delay, bs.CallPartial(setattr, self, '_pause_timer', None)) + bs.displaytimer(delay, bs.CallPartial(setattr, gnode, 'paused', False)) + + # Slow Motion + gnode.slow_motion = True + self._slow_motion_timer = bs.DisplayTimer(0.01, bs.CallPartial(setattr, gnode, 'slow_motion', True), True) + + delay += 0.14 + + bs.displaytimer(delay, bs.CallPartial(setattr, self, '_slow_motion_timer', None)) + bs.displaytimer(delay, bs.CallPartial(setattr, gnode, 'slow_motion', activity.slow_motion)) + + Blast.__init__ = new_init + + # Explosion Spaz Coloring + org_handlemessage = Blast.handlemessage + + def new_handlemessage(self: Blast, msg: Any, *args, **kwargs) -> Any: + org_handlemessage(self, msg, *args, **kwargs) + + if isinstance(msg, ExplodeHitMessage): + node = bs.getcollision().opposingnode + delegate = node.getdelegate(PlayerSpaz) or node.getdelegate(SpazBot) + if (delegate and not delegate.shield) and not node.invincible: + if not hasattr(delegate, '_original_color'): + delegate._original_color = node.color + delegate._original_highlight = node.highlight + + original_color = delegate._original_color + original_highlight = delegate._original_highlight + + blend_time = 1.0 + start_time = 6.0 + end_time = 7.0 + + bs.emitfx( + position=node.position, + velocity=node.velocity, + count=int(4.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='ice' if self.blast_type == 'ice' else 'smoke', + ) + + explosion_intensity = 0.70 if not self.blast_type == 'tnt' else 0.96 + + if self.blast_type == 'ice': + explosion_intensity = 0.91 + explosion_color = (0.07, 0.6, 1.5) + elif self.blast_type == 'sticky': + explosion_intensity = 0.91 + explosion_color = (0.07, 0.9, 0.07) + blend_time = 1.0 + start_time = 1.2 + end_time = 3.0 + else: + explosion_color = (0.07, 0.03, 0.0) + + + blend_color = blend_toward(node.color, explosion_color, explosion_intensity, True) + blend_highlight = blend_toward(node.highlight, explosion_color, explosion_intensity-0.03, True) + + bs.animate_array(node, 'color', 3, { + blend_time: blend_color, + start_time: blend_color, + end_time: original_color + }) + bs.animate_array(node, 'highlight', 3, { + blend_time: blend_highlight, + start_time: blend_highlight, + end_time: original_highlight + }) + + delegate._color_timer = bs.Timer(end_time, bs.CallPartial(delattr, delegate, '_original_color')) + delegate._highlight_timer = bs.Timer(end_time, bs.CallPartial(delattr, delegate, '_original_highlight')) + + Blast.handlemessage = new_handlemessage \ No newline at end of file diff --git a/plugins/utilities/sleep_on_afk.py b/plugins/utilities/sleep_on_afk.py new file mode 100644 index 0000000..7c14354 --- /dev/null +++ b/plugins/utilities/sleep_on_afk.py @@ -0,0 +1,116 @@ +# ba_meta require api 9 +from __future__ import annotations + +import babase +import bascenev1 as bs + +from bascenev1lib.actor.spaz import Spaz + +plugman = dict( + plugin_name="sleep_on_afk", + description="Staying idle for 40 seconds will make your character fall asleep, they need rest too..", + external_url="", + authors=[ + {"name": "DinoWattz", "email": "", "discord": ""} + ], + version="1.0.0", +) + +INGAME_TIME = 40 # (in seconds) + +# Spaz Changes +def _afk_sleep_knockout(self, value: float) -> None: + if not self.node: + return + if not self.is_alive() and hasattr(self.getplayer(self), 'sessionplayer'): + current_activity = self.getactivity() + player = self.getplayer(self).sessionplayer + #Pop timer data if the player is dead + if current_activity.customdata.get(str(player.id) +'_knockoutTimer') is not None: + current_activity.customdata.pop(str(player.id) +'_knockoutTimer') + return + + self.node.handlemessage('knockout', value) + +Spaz._afk_sleep_knockout = _afk_sleep_knockout # type: ignore + +# Idle Checker +def idle_start(activity: bs.Activity): + activity.customdata['afk_timer'] = bs.Timer(0.5, bs.CallStrict(idle_check, activity), repeat=True) + +def idle_check(current_activity: bs.Activity): + current_session = current_activity.session + wait_time = INGAME_TIME if not current_activity.slow_motion else round(INGAME_TIME / 3) + current = bs.time() * 1000 + if not current_session: + return + for player in current_session.sessionplayers: + if ( + not player.exists() + or not player.in_game + or not getattr(player, 'activityplayer', None) + or not getattr(player.activityplayer, 'actor', None) + or not getattr(player.activityplayer.actor, 'node', None) + or not player.activityplayer.is_alive() + or not player.activityplayer.actor.node + or not player.activityplayer.actor.node.exists() + and not getattr(player.activityplayer.actor.node, 'invincible', False) + ): + continue + + player_actor = player.activityplayer.actor + player_node = player_actor.node + player_data = player.activityplayer.customdata + player_turbo_times = player_actor._turbo_filter_times + + if player_node.move_up_down != 0.0 or player_node.move_left_right != 0.0: + player_data['last_input'] = current + elif player_turbo_times: + highest_turbo_time = player_turbo_times.get(max(player_turbo_times, key=player_turbo_times.get)) + if highest_turbo_time > player_data.get('last_input', current): + player_data.update({'last_input': highest_turbo_time }) + player_data['last_input'] = player_data.get('last_input', current) + + last_input = max(0, player_data['last_input']) + afk_time = int((current - last_input) / 1000) + + #print(player_data) + #print(last_input) + #print(current) + #print(player.getname() + ": " + str(afk_time)) + #print(wait_time) + + if afk_time >= wait_time: + current_activity.customdata[str(player.id) +'_knockoutTimer'] = bs.Timer(0.1, bs.WeakCallStrict(player_actor._afk_sleep_knockout, 100.0), repeat=True) + + # Make the player's node not an area of interest if it was one + if not getattr(player_actor, '_previous_is_area_of_interest', False): + player_actor._previous_is_area_of_interest = player_node.is_area_of_interest + + if player_actor._previous_is_area_of_interest: + player_node.is_area_of_interest = False + + elif current_activity.customdata.get(str(player.id) +'_knockoutTimer') is not None: + current_activity.customdata.pop(str(player.id) +'_knockoutTimer') + + # Restore the player's node area of interest if necessary + if getattr(player_actor, '_previous_is_area_of_interest', False): + player_node.is_area_of_interest = True + + if hasattr(player_actor, "_previous_is_area_of_interest"): + delattr(player_actor, "_previous_is_area_of_interest") + +# Setup new activity +org_on_begin = bs.Activity.on_begin + +def patched_on_begin(self, *args, **kwargs): + idle_start(self) + + return org_on_begin(self, *args, **kwargs) + +bs.Activity.on_begin = patched_on_begin + +# ba_meta export babase.Plugin + +class Plugin(babase.Plugin): + pass From e67d89171651a05158629cf25e59b3fb2ddd0e85 Mon Sep 17 00:00:00 2001 From: DinoWattz <116862698+DinoWattz@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:18:30 +0000 Subject: [PATCH 18/84] [ci] apply-plugin-metadata-and-formatting --- plugins/utilities.json | 56 +++ plugins/utilities/better_camera_shake.py | 567 ++++++++++++----------- plugins/utilities/chat_bubbles.py | 179 +++---- plugins/utilities/enhanced_effects.py | 277 +++++------ plugins/utilities/sleep_on_afk.py | 67 +-- 5 files changed, 627 insertions(+), 519 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 0c62bac..cf8f0e7 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2426,6 +2426,62 @@ "md5sum": "d8002dfc93be74d71f8e6e4aacfe6fd1" } } + }, + "better_camera_shake": { + "description": "This plugin makes the camera only shake when an explosion hits a player character, begone excessive camera shaking!", + "external_url": "", + "authors": [ + { + "name": "DinoWattz", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } + }, + "chat_bubbles": { + "description": "Adds whatever the players say above their character, includes a few other features too!", + "external_url": "", + "authors": [ + { + "name": "DinoWattz", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } + }, + "enhanced_effects": { + "description": "Explosions affect character colors, slow-mo tnt, fair colored shields, a trippy background screen and maybe more in the future!", + "external_url": "", + "authors": [ + { + "name": "DinoWattz", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } + }, + "sleep_on_afk": { + "description": "Staying idle for 40 seconds will make your character fall asleep, they need rest too..", + "external_url": "", + "authors": [ + { + "name": "DinoWattz", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } } } } \ No newline at end of file diff --git a/plugins/utilities/better_camera_shake.py b/plugins/utilities/better_camera_shake.py index 634fb1a..d176f46 100644 --- a/plugins/utilities/better_camera_shake.py +++ b/plugins/utilities/better_camera_shake.py @@ -26,356 +26,363 @@ plugman = dict( ) # Camera shake only on player impact + + def new__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', - ): - """Instantiate with given values.""" + 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', +): + """Instantiate with given values.""" - # bah; get off my lawn! - # pylint: disable=too-many-locals - # pylint: disable=too-many-statements + # bah; get off my lawn! + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements - super(type(self), self).__init__() + super(type(self), self).__init__() - shared = SharedObjects.get() - factory = BombFactory.get() + 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 + 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, - }, - ) + # 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) + 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) + # 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) + 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', - ) + if self.blast_type != 'ice': bs.emitfx( position=position, velocity=velocity, - count=int(4.0 + random.random() * 4), + count=int(1.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, + 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': + # 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', - ) + 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) + # It looks better if we delay a bit. + bs.timer(0.05, emit) - elif self.blast_type == 'sticky': + elif self.blast_type == 'sticky': - def emit() -> None: + 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), - spread=0.7, - chunk_type='slime', + chunk_type='rock', ) 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', + 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', + ) - # It looks better if we delay a bit. - bs.timer(0.05, emit) + # TNT throws splintery chunks. + if self.blast_type == 'tnt': - 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': + def emit_splinters() -> None: bs.emitfx( position=position, velocity=velocity, - count=int(4.0 + random.random() * 8), - chunk_type='rock', + 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(4.0 + random.random() * 8), - scale=0.5, - chunk_type='rock', + count=int(10.0 + random.random() * 20), + scale=0.8, + spread=1.5, + chunk_type='spark', ) - 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': + bs.timer(0.02, emit_extra_sparks) - 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', - ) + # It looks better if we delay a bit. + bs.timer(0.05, emit) - bs.timer(0.01, emit_splinters) + 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, + }, + ) - # Every now and then do a sparky one. - if self.blast_type == 'tnt' or random.random() < 0.1: + 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 - 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', - ) + 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) - bs.timer(0.02, emit_extra_sparks) + # 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) - # It looks better if we delay a bit. - bs.timer(0.05, emit) + bs.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) + bs.timer(13.0, scorch.delete) - 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, - }, - ) + if self.blast_type == 'ice': + factory.hiss_sound.play(position=light.position) - 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 + lpos = light.position + factory.random_explode_sound().play(position=lpos) + factory.debris_fall_sound.play(position=lpos) - 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) + # ↓ We don't need any of this, we're fully trained professionals. + # bs.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0) - # 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 + # TNT is more epic. + if self.blast_type == 'tnt': factory.random_explode_sound().play(position=lpos) - factory.debris_fall_sound.play(position=lpos) - # ↓ We don't need any of this, we're fully trained professionals. - #bs.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0) - - # TNT is more epic. - if self.blast_type == 'tnt': + def _extra_boom() -> None: factory.random_explode_sound().play(position=lpos) - def _extra_boom() -> None: - factory.random_explode_sound().play(position=lpos) + bs.timer(0.25, _extra_boom) - 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) - 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) - bs.timer(0.4, _extra_debris_sound) bomb.Blast.__init__ = new__init__ org_handlemessage = bomb.Blast.handlemessage + + def new_handlemessage(self, msg: Any, *args, **kwargs) -> Any: org_handlemessage(self, msg, *args, **kwargs) if isinstance(msg, ExplodeHitMessage): - node = bs.getcollision().opposingnode - delegate = node.getdelegate(PlayerSpaz) - has_used_camerashake = getattr(self, 'has_used_camerashake', False) - if not has_used_camerashake and (delegate and not delegate.shield) and not node.invincible: - self.has_used_camerashake = True - bs.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0) + node = bs.getcollision().opposingnode + delegate = node.getdelegate(PlayerSpaz) + has_used_camerashake = getattr(self, 'has_used_camerashake', False) + if not has_used_camerashake and (delegate and not delegate.shield) and not node.invincible: + self.has_used_camerashake = True + bs.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0) bomb.Blast.handlemessage = new_handlemessage # ba_meta export babase.Plugin + + class Plugin(babase.Plugin): - pass \ No newline at end of file + pass diff --git a/plugins/utilities/chat_bubbles.py b/plugins/utilities/chat_bubbles.py index 446ad07..af7eb31 100644 --- a/plugins/utilities/chat_bubbles.py +++ b/plugins/utilities/chat_bubbles.py @@ -23,8 +23,9 @@ plugman = dict( version="1.0.0", ) -PRINT_CHAT_MESSAGES = True # Prints chat messages to the console as "[Character's Name] Player 1/Player 2: Hello World!" -BUBBLE_DURATION: float = 4.7 # 4.7 matches the default onscreen message duration +# Prints chat messages to the console as "[Character's Name] Player 1/Player 2: Hello World!" +PRINT_CHAT_MESSAGES = True +BUBBLE_DURATION: float = 4.7 # 4.7 matches the default onscreen message duration SPEAK_VOLUME = 1.5 SHOUT_VOLUME = 3.0 @@ -35,13 +36,13 @@ CELEBRATE_MESSAGE = [(bs.CelebrateMessage, {"duration": 1.0})] EASTER_EGGS = [ { "triggers": { - "hi", "hello", "hey", "hiya", "yo", "sup", "heya", "howdy", - "greetings", "salutations", "ahoy", "ahoyhoy", "bonjour", - "hola", "ciao", "ola", "oi", "eae", "salve", + "hi", "hello", "hey", "hiya", "yo", "sup", "heya", "howdy", + "greetings", "salutations", "ahoy", "ahoyhoy", "bonjour", + "hola", "ciao", "ola", "oi", "eae", "salve", "bye", "goodbye", "see ya", "see you", "cya", "adios", "chau", "chao", "tchau", "adeus", "ate mais", "falou", "flw" - }, + }, "node_messages": [("celebrate_r", 1000)], }, { @@ -229,6 +230,8 @@ EASTER_EGGS = [ ] # Chat bubble functionality + + def create_bubble(player: bs.SessionPlayer, text: str, name: str): msg = normalize_message(text) is_caps = len(text) > 1 and text.isupper() @@ -240,11 +243,14 @@ def create_bubble(player: bs.SessionPlayer, text: str, name: str): def _fallback_logic(): nonlocal handled, activityplayer color = (1, 1, 0.4) - image_icon = {'texture': bs.gettexture("cuteSpaz"), 'tint_texture': bs.gettexture("cuteSpaz"), 'tint_color': (1.0, 1.0, 1.0), 'tint2_color': (1.0, 1.0, 1.0)} + image_icon = {'texture': bs.gettexture("cuteSpaz"), 'tint_texture': bs.gettexture( + "cuteSpaz"), 'tint_color': (1.0, 1.0, 1.0), 'tint2_color': (1.0, 1.0, 1.0)} if activityplayer: - normal_highlight = bs.normalized_color( bs.safecolor(player.highlight, target_intensity=1.15) ) - saturated_highlight = bs.normalized_color( bs.safecolor(player.highlight, target_intensity=0.75) ) + normal_highlight = bs.normalized_color( + bs.safecolor(player.highlight, target_intensity=1.15)) + saturated_highlight = bs.normalized_color( + bs.safecolor(player.highlight, target_intensity=0.75)) color = saturated_highlight if is_caps else normal_highlight image_icon = activityplayer.get_icon() @@ -252,7 +258,7 @@ def create_bubble(player: bs.SessionPlayer, text: str, name: str): color=color, top=True, image=image_icon) - + for egg in EASTER_EGGS: if msg in egg["triggers"]: if egg.get("sound"): @@ -298,7 +304,7 @@ def create_bubble(player: bs.SessionPlayer, text: str, name: str): activity = actor.getactivity() if activity is None or getattr(activity, 'expired', False): continue - + with activity.context: # Calculate scale based on text length base_scale = 0.015 @@ -312,23 +318,25 @@ def create_bubble(player: bs.SessionPlayer, text: str, name: str): }) char_node.connectattr('torso_position', mnode, 'input2') - normal_highlight = bs.normalized_color( bs.safecolor(player.highlight, target_intensity=1.15) ) - saturated_highlight = bs.normalized_color( bs.safecolor(player.highlight, target_intensity=0.75) ) + normal_highlight = bs.normalized_color( + bs.safecolor(player.highlight, target_intensity=1.15)) + saturated_highlight = bs.normalized_color( + bs.safecolor(player.highlight, target_intensity=0.75)) bubble_color = saturated_highlight if is_caps else normal_highlight textnode = bs.newnode('text', - owner=char_node, - attrs={ - 'text': text, - 'color': bubble_color, - 'shadow': 0.5, - 'flatness': 0.5, - 'scale': scale, - 'h_align': 'center', - 'v_align': 'bottom', - 'in_world': True, - 'opacity': 1.0, - }) + owner=char_node, + attrs={ + 'text': text, + 'color': bubble_color, + 'shadow': 0.5, + 'flatness': 0.5, + 'scale': scale, + 'h_align': 'center', + 'v_align': 'bottom', + 'in_world': True, + 'opacity': 1.0, + }) mnode.connectattr('output', textnode, 'position') @@ -366,7 +374,8 @@ def create_bubble(player: bs.SessionPlayer, text: str, name: str): sound = random.choice(pickup_sounds) if sound: - _play_chat_sound(actor, sound, SHOUT_VOLUME if is_caps else SPEAK_VOLUME) + _play_chat_sound( + actor, sound, SHOUT_VOLUME if is_caps else SPEAK_VOLUME) # Animation logic if egg.get('messages') and actor.is_alive() and hasattr(actor, 'handlemessage'): @@ -417,12 +426,13 @@ def create_bubble(player: bs.SessionPlayer, text: str, name: str): b for b in activityplayer.customdata['chat_bubbles'] if b[0] != textnode ] bs.timer(bubble_duration, cleanup) - + if not bubble_created: # No valid actor found: play sound if possible _fallback_logic() return + def update_bubble_offsets(bubbles): for i, (tnode, mnode) in enumerate(reversed(bubbles)): y_offset = 1.2 + 0.25 * i @@ -430,6 +440,8 @@ def update_bubble_offsets(bubbles): mnode.input1 = (0, y_offset, 0) # Tools + + def normalize_message(text): msg = text.strip().lower() msg = msg.translate(str.maketrans('', '', string.punctuation)) @@ -437,27 +449,30 @@ def normalize_message(text): c for c in unicodedata.normalize('NFD', msg) if unicodedata.category(c) != 'Mn' ) - + return msg # ba_meta export babase.Plugin + + class Plugin(babase.Plugin): def on_app_running(self) -> None: # Hook into chat message filtering # We're delaying until the app runs and using a timer so it's more likely to work with other chat plugins (please don't make a chat plugin/filter that is delayed like this). bs.apptimer(0.001, bs.WeakCallStrict(self.apply_patch)) - + def apply_patch(self): import bascenev1._hooks as _hooks - + _org_filter_chat_message = _hooks.filter_chat_message + def _chat_bubbles_hook(msg: str, client_id: int, *args, **kwargs) -> str | None: org_msg = _org_filter_chat_message(msg, client_id, *args, **kwargs) - + if org_msg is None: # The original message has been ignored, so we should ignore it as well return org_msg - + # Chat bubble sorcery try: roster = bs.get_game_roster() @@ -478,42 +493,43 @@ class Plugin(babase.Plugin): player_list.append(player) continue if player_list: - # Get combined name for multiple players on same client - name = '' - created_bubble = False - - for player in player_list: - if name == '': - name = player.getname() - else: - name = name + '/' + player.getname() + # Get combined name for multiple players on same client + name = '' + created_bubble = False - # Create chat bubble - for player in player_list: - activityplayer: bs.Player = player.activityplayer - # We have a session player but not an activity player - if not activityplayer: - if not dead_bubble: - create_bubble(player, org_msg, name) - created_bubble = dead_bubble = True - continue - # We have a dead activity player - if not activityplayer.is_alive() and not dead_bubble: + for player in player_list: + if name == '': + name = player.getname() + else: + name = name + '/' + player.getname() + + # Create chat bubble + for player in player_list: + activityplayer: bs.Player = player.activityplayer + # We have a session player but not an activity player + if not activityplayer: + if not dead_bubble: create_bubble(player, org_msg, name) created_bubble = dead_bubble = True - character = activityplayer.character if character is None else character - # elif we have an alive activity player - elif activityplayer.is_alive(): - create_bubble(player, org_msg, name) - created_bubble = True - character = activityplayer.character if character is None else character + continue + # We have a dead activity player + if not activityplayer.is_alive() and not dead_bubble: + create_bubble(player, org_msg, name) + created_bubble = dead_bubble = True + character = activityplayer.character if character is None else character + # elif we have an alive activity player + elif activityplayer.is_alive(): + create_bubble(player, org_msg, name) + created_bubble = True + character = activityplayer.character if character is None else character - if created_bubble: - matched = True - if PRINT_CHAT_MESSAGES: - print(f"[{character}] {name}: {org_msg}") - else: - bs.logging.warning("Failed to create a chat bubble, using non-session player method now.") + if created_bubble: + matched = True + if PRINT_CHAT_MESSAGES: + print(f"[{character}] {name}: {org_msg}") + else: + bs.logging.warning( + "Failed to create a chat bubble, using non-session player method now.") # Non-session players (in lobby or just spectating, this only works for servers) if not matched and not dead_bubble: for client in roster: @@ -537,41 +553,42 @@ class Plugin(babase.Plugin): class Focus(bs.Actor): def __init__( - self, - *, - owner: bs.Node | None = None, - ): + self, + *, + owner: bs.Node | None = None, + ): super().__init__() self._focusnode_material = bs.Material() self._focusnode_material.add_actions( - actions=('modify_node_collision', 'collide', False), - ) - + actions=('modify_node_collision', 'collide', False), + ) + if getattr(owner, 'owner', None): assert owner is not None - area_of_interest = False if getattr(owner.owner, 'is_area_of_interest', False) else True + area_of_interest = False if getattr( + owner.owner, 'is_area_of_interest', False) else True else: area_of_interest = False if getattr(owner, 'is_area_of_interest', False) else True # I'm tired of this area_of_interest nonsense, it never works right - #area_of_interest = False + # area_of_interest = False # actually it might work now that we don't spawn bubbles for dead players. self.node = bs.newnode( - 'prop', - delegate=self, - owner=owner, - attrs={ - 'body': 'sphere', + 'prop', + delegate=self, + owner=owner, + attrs={ + 'body': 'sphere', 'body_scale': 0.0, 'shadow_size': 0.0, 'gravity_scale': 0.0, 'is_area_of_interest': area_of_interest, 'materials': (self._focusnode_material,), - }, - ) - + }, + ) + @override def handlemessage(self, msg: Any) -> Any: assert not self.expired diff --git a/plugins/utilities/enhanced_effects.py b/plugins/utilities/enhanced_effects.py index 9a33534..d78f561 100644 --- a/plugins/utilities/enhanced_effects.py +++ b/plugins/utilities/enhanced_effects.py @@ -25,92 +25,95 @@ plugman = dict( # Transparent Background (kinda hacky, works best on pc with high/higher quality 'visuals' setting) BACKGROUND_OPACITY = 0.5 + + def __modified_background_init( - self, - fade_time: float = 0.5, - start_faded: bool = False, - show_logo: bool = False, - ): - super(type(self), self).__init__() - self._dying = False - self.fade_time = fade_time - # We're special in that we create our node in the session - # scene instead of the activity scene. - # This way we can overlap multiple activities for fades - # and whatnot. - session = bs.getsession() - self._session = weakref.ref(session) - with session.context: - self.node = bs.newnode( + self, + fade_time: float = 0.5, + start_faded: bool = False, + show_logo: bool = False, +): + super(type(self), self).__init__() + self._dying = False + self.fade_time = fade_time + # We're special in that we create our node in the session + # scene instead of the activity scene. + # This way we can overlap multiple activities for fades + # and whatnot. + session = bs.getsession() + self._session = weakref.ref(session) + with session.context: + self.node = bs.newnode( + 'image', + delegate=self, + attrs={ + 'fill_screen': True, + 'texture': bs.gettexture('bg'), + 'tilt_translate': -0.3, + 'has_alpha_channel': False, + 'color': (1, 1, 1), + 'opacity': BACKGROUND_OPACITY, + }, + ) + if not start_faded: + bs.animate( + self.node, + 'opacity', + {0.0: 0.0, self.fade_time: BACKGROUND_OPACITY}, + loop=False, + ) + if show_logo: + logo_texture = bs.gettexture('logo') + logo_mesh = bs.getmesh('logo') + logo_mesh_transparent = bs.getmesh('logoTransparent') + self.logo = bs.newnode( 'image', - delegate=self, + owner=self.node, attrs={ - 'fill_screen': True, - 'texture': bs.gettexture('bg'), - 'tilt_translate': -0.3, - 'has_alpha_channel': False, - 'color': (1, 1, 1), - 'opacity': BACKGROUND_OPACITY, + 'texture': logo_texture, + 'mesh_opaque': logo_mesh, + 'mesh_transparent': logo_mesh_transparent, + 'scale': (0.7, 0.7), + 'vr_depth': -250, + 'color': (0.15, 0.15, 0.15), + 'position': (0, 0), + 'tilt_translate': -0.05, + 'absolute_scale': False, }, ) - if not start_faded: - bs.animate( - self.node, - 'opacity', - {0.0: 0.0, self.fade_time: BACKGROUND_OPACITY}, - loop=False, + self.node.connectattr('opacity', self.logo, 'opacity') + # add jitter/pulse for a stop-motion-y look unless we're in VR + # in which case stillness is better + if not bs.app.env.vr: + self.cmb = bs.newnode( + 'combine', owner=self.node, attrs={'size': 2} ) - if show_logo: - logo_texture = bs.gettexture('logo') - logo_mesh = bs.getmesh('logo') - logo_mesh_transparent = bs.getmesh('logoTransparent') - self.logo = bs.newnode( - 'image', - owner=self.node, - attrs={ - 'texture': logo_texture, - 'mesh_opaque': logo_mesh, - 'mesh_transparent': logo_mesh_transparent, - 'scale': (0.7, 0.7), - 'vr_depth': -250, - 'color': (0.15, 0.15, 0.15), - 'position': (0, 0), - 'tilt_translate': -0.05, - 'absolute_scale': False, - }, + for attr in ['input0', 'input1']: + bs.animate( + self.cmb, + attr, + {0.0: 0.693, 0.05: 0.7, 0.5: 0.693}, + loop=True, + ) + self.cmb.connectattr('output', self.logo, 'scale') + cmb = bs.newnode( + 'combine', owner=self.node, attrs={'size': 2} ) - self.node.connectattr('opacity', self.logo, 'opacity') - # add jitter/pulse for a stop-motion-y look unless we're in VR - # in which case stillness is better - if not bs.app.env.vr: - self.cmb = bs.newnode( - 'combine', owner=self.node, attrs={'size': 2} - ) - for attr in ['input0', 'input1']: - bs.animate( - self.cmb, - attr, - {0.0: 0.693, 0.05: 0.7, 0.5: 0.693}, - loop=True, - ) - self.cmb.connectattr('output', self.logo, 'scale') - cmb = bs.newnode( - 'combine', owner=self.node, attrs={'size': 2} - ) - cmb.connectattr('output', self.logo, 'position') - # Gen some random keys for that stop-motion-y look. - keys = {} - timeval = 0.0 - for _i in range(10): - keys[timeval] = (random.random() - 0.5) * 0.0015 - timeval += random.random() * 0.1 - bs.animate(cmb, 'input0', keys, loop=True) - keys = {} - timeval = 0.0 - for _i in range(10): - keys[timeval] = (random.random() - 0.5) * 0.0015 + 0.05 - timeval += random.random() * 0.1 - bs.animate(cmb, 'input1', keys, loop=True) + cmb.connectattr('output', self.logo, 'position') + # Gen some random keys for that stop-motion-y look. + keys = {} + timeval = 0.0 + for _i in range(10): + keys[timeval] = (random.random() - 0.5) * 0.0015 + timeval += random.random() * 0.1 + bs.animate(cmb, 'input0', keys, loop=True) + keys = {} + timeval = 0.0 + for _i in range(10): + keys[timeval] = (random.random() - 0.5) * 0.0015 + 0.05 + timeval += random.random() * 0.1 + bs.animate(cmb, 'input1', keys, loop=True) + def _modified_background_die(self, immediate: bool = False) -> None: session = self._session() @@ -138,10 +141,13 @@ def _modified_background_die(self, immediate: bool = False) -> None: ) bs.timer(self.fade_time + 0.1, self.node.delete) + Background.__init__ = __modified_background_init Background._die = _modified_background_die # Tools + + def blend_toward( rgb: tuple[float, float, float], target: tuple[float, float, float], @@ -177,6 +183,8 @@ def blend_toward( return tuple(clamp01(linear_to_srgb(c)) for c in blended_lin) # ba_meta export babase.Plugin + + class Plugin(babase.Plugin): def on_app_running(self) -> None: from bascenev1lib.actor.bomb import Blast, ExplodeHitMessage @@ -190,7 +198,8 @@ class Plugin(babase.Plugin): def equip_shields(self, decay: bool = False) -> None: org_equip_shields(self, decay) if self.shield is not None: - safe_highlight = bs.safecolor(getattr(self, '_original_highlight', self.node.highlight), target_intensity=0.75) + safe_highlight = bs.safecolor( + getattr(self, '_original_highlight', self.node.highlight), target_intensity=0.75) self.shield.color = bs.normalized_color(safe_highlight) Spaz.equip_shields = equip_shields @@ -208,7 +217,8 @@ class Plugin(babase.Plugin): # Pause gnode.paused = True - self._pause_timer = bs.DisplayTimer(0.01, bs.CallPartial(setattr, gnode, 'paused', True), True) + self._pause_timer = bs.DisplayTimer( + 0.01, bs.CallPartial(setattr, gnode, 'paused', True), True) delay = 0.06 @@ -217,12 +227,14 @@ class Plugin(babase.Plugin): # Slow Motion gnode.slow_motion = True - self._slow_motion_timer = bs.DisplayTimer(0.01, bs.CallPartial(setattr, gnode, 'slow_motion', True), True) + self._slow_motion_timer = bs.DisplayTimer( + 0.01, bs.CallPartial(setattr, gnode, 'slow_motion', True), True) delay += 0.14 bs.displaytimer(delay, bs.CallPartial(setattr, self, '_slow_motion_timer', None)) - bs.displaytimer(delay, bs.CallPartial(setattr, gnode, 'slow_motion', activity.slow_motion)) + bs.displaytimer(delay, bs.CallPartial( + setattr, gnode, 'slow_motion', activity.slow_motion)) Blast.__init__ = new_init @@ -233,58 +245,61 @@ class Plugin(babase.Plugin): org_handlemessage(self, msg, *args, **kwargs) if isinstance(msg, ExplodeHitMessage): - node = bs.getcollision().opposingnode - delegate = node.getdelegate(PlayerSpaz) or node.getdelegate(SpazBot) - if (delegate and not delegate.shield) and not node.invincible: - if not hasattr(delegate, '_original_color'): - delegate._original_color = node.color - delegate._original_highlight = node.highlight + node = bs.getcollision().opposingnode + delegate = node.getdelegate(PlayerSpaz) or node.getdelegate(SpazBot) + if (delegate and not delegate.shield) and not node.invincible: + if not hasattr(delegate, '_original_color'): + delegate._original_color = node.color + delegate._original_highlight = node.highlight - original_color = delegate._original_color - original_highlight = delegate._original_highlight + original_color = delegate._original_color + original_highlight = delegate._original_highlight + blend_time = 1.0 + start_time = 6.0 + end_time = 7.0 + + bs.emitfx( + position=node.position, + velocity=node.velocity, + count=int(4.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='ice' if self.blast_type == 'ice' else 'smoke', + ) + + explosion_intensity = 0.70 if not self.blast_type == 'tnt' else 0.96 + + if self.blast_type == 'ice': + explosion_intensity = 0.91 + explosion_color = (0.07, 0.6, 1.5) + elif self.blast_type == 'sticky': + explosion_intensity = 0.91 + explosion_color = (0.07, 0.9, 0.07) blend_time = 1.0 - start_time = 6.0 - end_time = 7.0 + start_time = 1.2 + end_time = 3.0 + else: + explosion_color = (0.07, 0.03, 0.0) - bs.emitfx( - position=node.position, - velocity=node.velocity, - count=int(4.0 + random.random() * 4), - emit_type='tendrils', - tendril_type='ice' if self.blast_type == 'ice' else 'smoke', - ) + blend_color = blend_toward(node.color, explosion_color, + explosion_intensity, True) + blend_highlight = blend_toward( + node.highlight, explosion_color, explosion_intensity-0.03, True) - explosion_intensity = 0.70 if not self.blast_type == 'tnt' else 0.96 - - if self.blast_type == 'ice': - explosion_intensity = 0.91 - explosion_color = (0.07, 0.6, 1.5) - elif self.blast_type == 'sticky': - explosion_intensity = 0.91 - explosion_color = (0.07, 0.9, 0.07) - blend_time = 1.0 - start_time = 1.2 - end_time = 3.0 - else: - explosion_color = (0.07, 0.03, 0.0) + bs.animate_array(node, 'color', 3, { + blend_time: blend_color, + start_time: blend_color, + end_time: original_color + }) + bs.animate_array(node, 'highlight', 3, { + blend_time: blend_highlight, + start_time: blend_highlight, + end_time: original_highlight + }) + delegate._color_timer = bs.Timer( + end_time, bs.CallPartial(delattr, delegate, '_original_color')) + delegate._highlight_timer = bs.Timer( + end_time, bs.CallPartial(delattr, delegate, '_original_highlight')) - blend_color = blend_toward(node.color, explosion_color, explosion_intensity, True) - blend_highlight = blend_toward(node.highlight, explosion_color, explosion_intensity-0.03, True) - - bs.animate_array(node, 'color', 3, { - blend_time: blend_color, - start_time: blend_color, - end_time: original_color - }) - bs.animate_array(node, 'highlight', 3, { - blend_time: blend_highlight, - start_time: blend_highlight, - end_time: original_highlight - }) - - delegate._color_timer = bs.Timer(end_time, bs.CallPartial(delattr, delegate, '_original_color')) - delegate._highlight_timer = bs.Timer(end_time, bs.CallPartial(delattr, delegate, '_original_highlight')) - - Blast.handlemessage = new_handlemessage \ No newline at end of file + Blast.handlemessage = new_handlemessage diff --git a/plugins/utilities/sleep_on_afk.py b/plugins/utilities/sleep_on_afk.py index 7c14354..3f3ad3b 100644 --- a/plugins/utilities/sleep_on_afk.py +++ b/plugins/utilities/sleep_on_afk.py @@ -16,28 +16,35 @@ plugman = dict( version="1.0.0", ) -INGAME_TIME = 40 # (in seconds) +INGAME_TIME = 40 # (in seconds) # Spaz Changes + + def _afk_sleep_knockout(self, value: float) -> None: - if not self.node: - return - if not self.is_alive() and hasattr(self.getplayer(self), 'sessionplayer'): - current_activity = self.getactivity() - player = self.getplayer(self).sessionplayer - #Pop timer data if the player is dead - if current_activity.customdata.get(str(player.id) +'_knockoutTimer') is not None: - current_activity.customdata.pop(str(player.id) +'_knockoutTimer') - return + if not self.node: + return + if not self.is_alive() and hasattr(self.getplayer(self), 'sessionplayer'): + current_activity = self.getactivity() + player = self.getplayer(self).sessionplayer + # Pop timer data if the player is dead + if current_activity.customdata.get(str(player.id) + '_knockoutTimer') is not None: + current_activity.customdata.pop(str(player.id) + '_knockoutTimer') + return - self.node.handlemessage('knockout', value) + self.node.handlemessage('knockout', value) -Spaz._afk_sleep_knockout = _afk_sleep_knockout # type: ignore + +Spaz._afk_sleep_knockout = _afk_sleep_knockout # type: ignore # Idle Checker + + def idle_start(activity: bs.Activity): - activity.customdata['afk_timer'] = bs.Timer(0.5, bs.CallStrict(idle_check, activity), repeat=True) - + activity.customdata['afk_timer'] = bs.Timer( + 0.5, bs.CallStrict(idle_check, activity), repeat=True) + + def idle_check(current_activity: bs.Activity): current_session = current_activity.session wait_time = INGAME_TIME if not current_activity.slow_motion else round(INGAME_TIME / 3) @@ -57,7 +64,7 @@ def idle_check(current_activity: bs.Activity): and not getattr(player.activityplayer.actor.node, 'invincible', False) ): continue - + player_actor = player.activityplayer.actor player_node = player_actor.node player_data = player.activityplayer.customdata @@ -66,22 +73,24 @@ def idle_check(current_activity: bs.Activity): if player_node.move_up_down != 0.0 or player_node.move_left_right != 0.0: player_data['last_input'] = current elif player_turbo_times: - highest_turbo_time = player_turbo_times.get(max(player_turbo_times, key=player_turbo_times.get)) + highest_turbo_time = player_turbo_times.get( + max(player_turbo_times, key=player_turbo_times.get)) if highest_turbo_time > player_data.get('last_input', current): - player_data.update({'last_input': highest_turbo_time }) + player_data.update({'last_input': highest_turbo_time}) player_data['last_input'] = player_data.get('last_input', current) - + last_input = max(0, player_data['last_input']) afk_time = int((current - last_input) / 1000) - #print(player_data) - #print(last_input) - #print(current) - #print(player.getname() + ": " + str(afk_time)) - #print(wait_time) - + # print(player_data) + # print(last_input) + # print(current) + # print(player.getname() + ": " + str(afk_time)) + # print(wait_time) + if afk_time >= wait_time: - current_activity.customdata[str(player.id) +'_knockoutTimer'] = bs.Timer(0.1, bs.WeakCallStrict(player_actor._afk_sleep_knockout, 100.0), repeat=True) + current_activity.customdata[str(player.id) + '_knockoutTimer'] = bs.Timer( + 0.1, bs.WeakCallStrict(player_actor._afk_sleep_knockout, 100.0), repeat=True) # Make the player's node not an area of interest if it was one if not getattr(player_actor, '_previous_is_area_of_interest', False): @@ -90,8 +99,8 @@ def idle_check(current_activity: bs.Activity): if player_actor._previous_is_area_of_interest: player_node.is_area_of_interest = False - elif current_activity.customdata.get(str(player.id) +'_knockoutTimer') is not None: - current_activity.customdata.pop(str(player.id) +'_knockoutTimer') + elif current_activity.customdata.get(str(player.id) + '_knockoutTimer') is not None: + current_activity.customdata.pop(str(player.id) + '_knockoutTimer') # Restore the player's node area of interest if necessary if getattr(player_actor, '_previous_is_area_of_interest', False): @@ -100,17 +109,21 @@ def idle_check(current_activity: bs.Activity): if hasattr(player_actor, "_previous_is_area_of_interest"): delattr(player_actor, "_previous_is_area_of_interest") + # Setup new activity org_on_begin = bs.Activity.on_begin + def patched_on_begin(self, *args, **kwargs): idle_start(self) return org_on_begin(self, *args, **kwargs) + bs.Activity.on_begin = patched_on_begin # ba_meta export babase.Plugin + class Plugin(babase.Plugin): pass From 963f7dfa878a57f6ae1dfe54c98f0485cfa4005c Mon Sep 17 00:00:00 2001 From: DinoWattz <116862698+DinoWattz@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:18:31 +0000 Subject: [PATCH 19/84] [ci] apply-version-metadata --- plugins/utilities.json | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index cf8f0e7..3759f5a 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2438,7 +2438,12 @@ } ], "versions": { - "1.0.0": null + "1.0.0": { + "api_version": 9, + "commit_sha": "e67d891", + "released_on": "23-02-2026", + "md5sum": "552b0ff0492bdab6cf69c1bc2d679e0a" + } } }, "chat_bubbles": { @@ -2452,7 +2457,12 @@ } ], "versions": { - "1.0.0": null + "1.0.0": { + "api_version": 9, + "commit_sha": "e67d891", + "released_on": "23-02-2026", + "md5sum": "307175b18b80671a7984862bcf09f1ce" + } } }, "enhanced_effects": { @@ -2466,7 +2476,12 @@ } ], "versions": { - "1.0.0": null + "1.0.0": { + "api_version": 9, + "commit_sha": "e67d891", + "released_on": "23-02-2026", + "md5sum": "63c665905dd4619d3f99ca8977f8fbf3" + } } }, "sleep_on_afk": { @@ -2480,7 +2495,12 @@ } ], "versions": { - "1.0.0": null + "1.0.0": { + "api_version": 9, + "commit_sha": "e67d891", + "released_on": "23-02-2026", + "md5sum": "5de7a474c3c6b21e8ea1e1abff3c07a1" + } } } } From 822208149cdb4de16190b3077d40a0acaeb825e6 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:29:39 +0530 Subject: [PATCH 20/84] fix for latest version --- CHANGELOG.md | 4 ++++ index.json | 1 + plugin_manager.py | 11 ++++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3603c84..45cca48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Plugin Manager (dd-mm-yyyy) +### 1.1.8 (23-02-2026) + +- Fix for bs 1.7.61 build no 22714 + ### 1.1.7 (15-02-2026) - Added function to fill form details for plugin bug report button diff --git a/index.json b/index.json index 3ee912d..6017b86 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,7 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { + "1.1.8": null, "1.1.7": { "api_version": 9, "commit_sha": "af881af", diff --git a/plugin_manager.py b/plugin_manager.py index 7e5b8a2..fce6017 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -26,7 +26,7 @@ from datetime import datetime # Modules used for overriding AllSettingsWindow import logging -PLUGIN_MANAGER_VERSION = "1.1.7" +PLUGIN_MANAGER_VERSION = "1.1.8" 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. @@ -43,7 +43,12 @@ HEADERS = { "User-Agent": _env["legacy_user_agent_string"], } PLUGIN_DIRECTORY = _env["python_directory_user"] -loop = babase._asyncio._asyncio_event_loop + +# compatibility for older API versions. +if _env.get("build_number", 0) < 22714: + babase._asyncio._g_asyncio_event_loop = babase._asyncio._asyncio_event_loop + +loop = babase._asyncio._g_asyncio_event_loop open_popups = [] @@ -3415,7 +3420,7 @@ class EntryPoint(babase.Plugin): from bauiv1lib.settings import allsettings allsettings.AllSettingsWindow = NewAllSettingsWindow DNSBlockWorkaround.apply() - asyncio.set_event_loop(babase._asyncio._asyncio_event_loop) + asyncio.set_event_loop(babase._asyncio._g_asyncio_event_loop) startup_tasks = StartupTasks() loop.create_task(startup_tasks.execute()) From ea78bf01db5f78c1d2cac995f786c60d686141ee Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:00:17 +0000 Subject: [PATCH 21/84] [ci] apply-version-metadata --- index.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.json b/index.json index 6017b86..f9bc733 100644 --- a/index.json +++ b/index.json @@ -1,7 +1,12 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { - "1.1.8": null, + "1.1.8": { + "api_version": 9, + "commit_sha": "8222081", + "released_on": "23-02-2026", + "md5sum": "8e8e9fc5b818883102c6081619404318" + }, "1.1.7": { "api_version": 9, "commit_sha": "af881af", From 0c22ef23b024f310d2bb43e372f65bd14fb3f221 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:47:36 +0530 Subject: [PATCH 22/84] add vanishing_tiles plugin --- plugins/utilities/vanishing_tiles.py | 480 +++++++++++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 plugins/utilities/vanishing_tiles.py diff --git a/plugins/utilities/vanishing_tiles.py b/plugins/utilities/vanishing_tiles.py new file mode 100644 index 0000000..898bf69 --- /dev/null +++ b/plugins/utilities/vanishing_tiles.py @@ -0,0 +1,480 @@ +# ba_meta require api 9 +from __future__ import annotations +from bascenev1lib.gameutils import SharedObjects +import bascenev1 as bs +import babase +import random +from typing import Optional, List, Dict, Any + +# Vanishing Tiles - BombSquad Minigame +# Tiles disappear one by one. Survive all rounds to win. +# Last player/team standing takes the crown. + +plugman = dict( + plugin_name="vanishing_tiles", + description="Tiles disappear gradually. Survive each round to continue. Last player standing wins!", + external_url="https://discord.gg/sQGDsztQcy", + authors=[ + {"name": "Mr.ghosty", "email": "", "discord": "gaurangbroyo"}, + {"name": "senchx", "email": "", "discord": "senchx0"}, + ], + version="2.0.0", +) + + +class Player(bs.Player['Team']): + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None + + +class Team(bs.Team[Player]): + pass + + +# ba_meta export bascenev1.GameActivity +class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): + + name = 'Vanishing Tiles' + description = 'Tiles disappear one by one. Be the last one standing!' + scoreconfig = bs.ScoreConfig(label='Survived', scoretype=bs.ScoreType.MILLISECONDS) + announce_player_deaths = True + + @classmethod + def get_available_settings(cls, sessiontype: type[bs.Session]) -> list[babase.Setting]: + return [ + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Show Credits', default=True), + ] + + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, (bs.FreeForAllSession, bs.DualTeamSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return ['Vanishing Tiles Arena'] + + def __init__(self, settings: dict) -> None: + super().__init__(settings) + self._epic_mode: bool = bool(settings.get('Epic Mode', False)) + self._show_credits: bool = bool(settings.get('Show Credits', True)) + self.default_music = bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL + + self._round: int = 1 + self._removing: bool = False + self._game_start_time: Optional[float] = None + + self._tile_nodes: Dict[int, bs.Node] = {} + self._region_nodes: Dict[int, bs.Node] = {} + self._present_tile_ids: set[int] = set() + + self._remove_speed: float = 1.8 + + self._collide_mat = bs.Material() + self._collide_mat.add_actions(actions=(('modify_part_collision', 'collide', True),)) + self._no_collide_mat = bs.Material() + self._no_collide_mat.add_actions(actions=(('modify_part_collision', 'collide', False),)) + + self._default_tex = bs.gettexture('powerupHealth') + self._warn_tex = bs.gettexture('powerupCurse') + self._final_tex = bs.gettexture('powerupPunch') + + self._hud_round: Optional[bs.Node] = None + self._hud_players: Optional[bs.Node] = None + self._hud_tiles: Optional[bs.Node] = None + self._credit_node: Optional[bs.Node] = None + + if self._epic_mode: + self.slow_motion = True + + def spawn_player(self, player: Player) -> Any: + if isinstance(self.session, bs.FreeForAllSession): + pos = VanishingTilesMapDefs.points['spawn1'] + spaz = self.spawn_player_spaz(player, position=pos) + else: + spaz = self.spawn_player_spaz(player) + spaz.connect_controls_to_player( + enable_punch=False, enable_pickup=False, enable_bomb=False) + spaz.set_bomb_count(0) + return spaz + + def on_begin(self) -> None: + super().on_begin() + self._game_start_time = bs.time() + + if self._show_credits: + self._credit_node = bs.newnode('text', attrs={ + 'text': 'Made by Mr.Ghosty', + 'scale': 0.7, + 'position': (0, 8), + 'shadow': 0.8, + 'flatness': 1.0, + 'color': (1.0, 0.3, 0.8, 1.0), + 'h_align': 'center', + 'v_attach': 'bottom', + }) + bs.animate_array(self._credit_node, 'color', 4, { + 0.0: [1.0, 0.3, 0.8, 1.0], + 1.0: [0.3, 0.8, 1.0, 1.0], + 2.0: [0.4, 1.0, 0.4, 1.0], + 3.0: [1.0, 1.0, 0.2, 1.0], + 4.0: [1.0, 0.3, 0.8, 1.0], + }, loop=True) + + self._build_hud() + self._cleanup_tiles() + self._spawn_all_tiles() + self._show_round_banner() + + if not isinstance(self.session, bs.FreeForAllSession): + if not all(len(t.players) >= 1 for t in self.teams): + bs.broadcastmessage('Not enough players — draw!', color=(1, 1, 0)) + bs.timer(1.0, bs.CallStrict(self.end, bs.GameResults())) + return + + bs.timer(1.0, self._check_initial_state) + bs.timer(3.0, self._start_removal) + + def _build_hud(self) -> None: + for attr in ('_hud_round', '_hud_players', '_hud_tiles'): + node = getattr(self, attr, None) + if node: + try: + node.delete() + except Exception: + pass + setattr(self, attr, None) + + self._hud_round = bs.newnode('text', attrs={ + 'text': '', 'scale': 0.85, 'position': (0, -40), + 'maxwidth': 300, 'h_align': 'center', 'v_align': 'center', + 'v_attach': 'top', 'h_attach': 'center', + 'shadow': 1.0, 'flatness': 1.0, 'color': (1, 1, 1, 1), + 'in_world': False, + }) + self._hud_players = bs.newnode('text', attrs={ + 'text': '', 'scale': 0.8, 'position': (0, -58), + 'maxwidth': 300, 'h_align': 'center', 'v_align': 'center', + 'v_attach': 'top', 'h_attach': 'center', + 'shadow': 1.0, 'flatness': 1.0, 'color': (1, 0.8, 0.2, 1), + 'in_world': False, + }) + self._hud_tiles = bs.newnode('text', attrs={ + 'text': '', 'scale': 0.8, 'position': (0, -74), + 'maxwidth': 300, 'h_align': 'center', 'v_align': 'center', + 'v_attach': 'top', 'h_attach': 'center', + 'shadow': 1.0, 'flatness': 1.0, 'color': (0.5, 1.0, 0.5, 1), + 'in_world': False, + }) + self._refresh_hud() + + def _refresh_hud(self) -> None: + alive = len([p for p in self.players if p.is_alive()]) + tiles = len(self._present_tile_ids) + urgent = tiles <= 4 + + if self._hud_round: + self._hud_round.text = f'Round {self._round}' + + if self._hud_players: + self._hud_players.text = f'Players alive: {alive}' + + if self._hud_tiles: + self._hud_tiles.color = (1.0, 0.3, 0.3, 1) if urgent else (0.5, 1.0, 0.5, 1) + self._hud_tiles.text = f'Tiles left: {tiles}' + + def _show_round_banner(self) -> None: + node = bs.newnode('text', attrs={ + 'text': f'Round {self._round}', + 'scale': 1.3, + 'position': (0, 60), + 'shadow': 1.2, + 'flatness': 0.7, + 'color': (1, 1, 0, 1), + 'h_align': 'center', + 'v_attach': 'center', + 'in_world': False, + }) + bs.animate(node, 'scale', {0: 0.0, 0.15: 1.3, 2.0: 1.3, 2.5: 0.0}) + bs.timer(2.6, bs.CallStrict(node.delete)) + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + player.death_time = bs.time() + bs.broadcastmessage( + f'{player.getname()} joined! Currently Round {self._round} — ' + f'{len(self._present_tile_ids)} tiles left.', + color=(0.5, 1.0, 1.0), + transient=True, + ) + return + self.spawn_player(player) + + def _check_initial_state(self) -> None: + if not [p for p in self.players if p.is_alive()]: + bs.timer(1.0, self._check_end_game) + + def _spawn_all_tiles(self) -> None: + positions = [ + (4.5, 2, -9), (4.5, 2, -6), (4.5, 2, -3), (4.5, 2, 0), + (1.5, 2, -9), (1.5, 2, -6), (1.5, 2, -3), (1.5, 2, 0), + (-1.5, 2, -9), (-1.5, 2, -6), (-1.5, 2, -3), (-1.5, 2, 0), + (-4.5, 2, -9), (-4.5, 2, -6), (-4.5, 2, -3), (-4.5, 2, 0), + ] + model = bs.getmesh('buttonSquareOpaque') + shared = SharedObjects.get() + for i, pos in enumerate(positions): + tile = bs.newnode('prop', attrs={ + 'body': 'puck', + 'position': pos, + 'mesh': model, + 'mesh_scale': 3.73, + 'body_scale': 3.73, + 'gravity_scale': 0.0, + 'color_texture': self._default_tex, + 'reflection': 'soft', + 'materials': [self._no_collide_mat], + }) + region = bs.newnode('region', attrs={ + 'position': pos, + 'scale': (3.5, 0.1, 3.5), + 'type': 'box', + 'materials': [self._collide_mat, shared.footing_material], + }) + self._tile_nodes[i] = tile + self._region_nodes[i] = region + self._present_tile_ids.add(i) + self._refresh_hud() + + def _start_removal(self) -> None: + if not self._removing: + self._removing = True + self._remove_next_tile() + + def _make_final_tile(self) -> None: + try: + tile_id = list(self._present_tile_ids)[0] + tile = self._tile_nodes.get(tile_id) + if tile and tile.exists(): + tile.color_texture = self._final_tex + except Exception: + pass + + self.slow_motion = True + bs.broadcastmessage('LAST TILE!', color=(1, 0.4, 0.1)) + for p in self.players: + if p.is_alive() and p.actor and p.actor.exists(): + p.actor.node.handlemessage(bs.CelebrateMessage(3.0)) + + self._refresh_hud() + bs.timer(3.0, self._round_end_check) + + def _remove_next_tile(self) -> None: + if len(self._present_tile_ids) <= 1: + self._make_final_tile() + return + + urgent = len(self._present_tile_ids) <= 4 + if urgent: + bs.getsound('shieldDown').play(0.4) + + tile_id = random.choice(list(self._present_tile_ids)) + self._remove_tile(tile_id) + bs.timer(self._remove_speed, self._remove_next_tile) + + def _remove_tile(self, tile_id: int) -> None: + tile = self._tile_nodes.get(tile_id) + region = self._region_nodes.get(tile_id) + if tile is None or not tile.exists(): + self._present_tile_ids.discard(tile_id) + return + + tile.color_texture = self._warn_tex + + def vanish() -> None: + if tile.exists(): + bs.emitfx( + position=tile.position, + count=20, scale=0.9, spread=0.5, + chunk_type='spark', + ) + tile.delete() + if region and region.exists(): + region.delete() + self._present_tile_ids.discard(tile_id) + self._refresh_hud() + + bs.timer(1.0, vanish) + + def _round_end_check(self) -> None: + self.slow_motion = self._epic_mode + + if not isinstance(self.session, bs.FreeForAllSession): + living_teams = [ + t for t in self.teams if any(p.is_alive() for p in t.players)] + if len(living_teams) <= 1: + self._removing = False + self.end_game() + return + else: + if len([p for p in self.players if p.is_alive()]) <= 1: + self._removing = False + self.end_game() + return + + self._remove_speed = max(0.3, self._remove_speed * 0.75) + self._round += 1 + survivors = [p for p in self.players if p.is_alive()] + bs.broadcastmessage( + f'Round {self._round}! Speed increasing!', color=(0.5, 1.0, 1.0)) + bs.timer(1.5, bs.CallStrict(self._next_round, survivors)) + + def _next_round(self, survivors: List[Player]) -> None: + safety = bs.newnode('region', attrs={ + 'position': (0, 1.5, -5), + 'scale': (20, 0.2, 20), + 'type': 'box', + 'materials': [SharedObjects.get().footing_material], + }) + self._cleanup_tiles() + self._spawn_all_tiles() + self._show_round_banner() + self._refresh_hud() + + for p in survivors: + if p.actor and p.actor.exists(): + p.actor.handlemessage( + bs.StandMessage(VanishingTilesMapDefs.points['spawn1'])) + + bs.timer(2.5, bs.CallStrict(safety.delete)) + self._removing = False + bs.timer(3.0, self._start_removal) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) + p = msg.getplayer(Player) + p.death_time = bs.time() + self._refresh_hud() + bs.timer(0.5, self._check_end_game) + return None + return super().handlemessage(msg) + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + bs.timer(0.2, self._check_team_empty) + + def _check_team_empty(self) -> None: + if self.has_ended(): + return + if isinstance(self.session, bs.FreeForAllSession): + return + for team in self.teams: + if len(team.players) == 0: + winner = next((t for t in self.teams if t is not team), None) + bs.broadcastmessage('A team has no players — game over!', + color=(1, 1, 0)) + bs.timer(1.0, bs.CallStrict(self.end_game)) + return + + def _check_end_game(self) -> None: + if not self.has_begun(): + return + if not isinstance(self.session, bs.FreeForAllSession): + if len([t for t in self.teams + if any(p.is_alive() for p in t.players)]) <= 1: + self.end_game() + else: + if len([p for p in self.players if p.is_alive()]) == 0: + self.end_game() + + def end_game(self) -> None: + if self.has_ended(): + return + cur_time = bs.time() + start = self._game_start_time or cur_time + results = bs.GameResults() + for team in self.teams: + longest = 0.0 + for p in team.players: + death = p.death_time or (cur_time + 1) + longest = max(longest, death - start) + results.set_team_score(team, int(longest * 1000)) + + for attr in ('_hud_round', '_hud_players', '_hud_tiles'): + node = getattr(self, attr, None) + if node: + try: + node.delete() + except Exception: + pass + + if self._credit_node: + try: + self._credit_node.delete() + except Exception: + pass + + self.end(results=results) + + def _cleanup_tiles(self) -> None: + for n in list(self._tile_nodes.values()): + if n.exists(): + n.delete() + for n in list(self._region_nodes.values()): + if n.exists(): + n.delete() + self._tile_nodes.clear() + self._region_nodes.clear() + self._present_tile_ids.clear() + + +class VanishingTilesMapDefs: + points = {'spawn1': (0, 3, -5)} + boxes = { + 'area_of_interest_bounds': (0, 4, -5, 0, 0, 0, 16, 8, 16), + 'map_bounds': (0, 4, -5, 0, 0, 0, 30, 14, 30), + } + + +class VanishingTilesMap(bs.Map): + defs = VanishingTilesMapDefs() + name = 'Vanishing Tiles Arena' + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'powerupHealth' + + @classmethod + def on_preload(cls) -> Any: + return { + 'bgtex': bs.gettexture('menuBG'), + 'bgmesh': bs.getmesh('thePadBG'), + } + + def __init__(self) -> None: + super().__init__() + self.node = bs.newnode('terrain', attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'], + }) + + +bs._map.register_map(VanishingTilesMap) + + +# ba_meta export babase.Plugin +class Main(babase.Plugin): + def __init__(self) -> None: + babase.app.classic.add_coop_practice_level( + bs.Level( + name='Vanishing Tiles', + displayname='Vanishing Tiles', + gametype=VanishingTilesGame, + settings={}, + preview_texture_name='powerupHealth', + ) + ) From 38f9e35e53d033698d5606e0cc43d377d68cdf4d Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:18:19 +0000 Subject: [PATCH 23/84] [ci] apply-plugin-metadata-and-formatting --- plugins/utilities.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/plugins/utilities.json b/plugins/utilities.json index 3759f5a..6409042 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2502,6 +2502,25 @@ "md5sum": "5de7a474c3c6b21e8ea1e1abff3c07a1" } } + }, + "vanishing_tiles": { + "description": "Tiles disappear gradually. Survive each round to continue. Last player standing wins!", + "external_url": "https://discord.gg/sQGDsztQcy", + "authors": [ + { + "name": "Mr.ghosty", + "email": "", + "discord": "gaurangbroyo" + }, + { + "name": "senchx", + "email": "", + "discord": "senchx0" + } + ], + "versions": { + "2.0.0": null + } } } } \ No newline at end of file From 56ad1aa843b620cc8bb182360960dfca2317195a Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:18:20 +0000 Subject: [PATCH 24/84] [ci] apply-version-metadata --- plugins/utilities.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 6409042..5a24e31 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2519,7 +2519,12 @@ } ], "versions": { - "2.0.0": null + "2.0.0": { + "api_version": 9, + "commit_sha": "38f9e35", + "released_on": "09-03-2026", + "md5sum": "f2b1b542cf8cb7b633c5b460966bd26c" + } } } } From 4f0cdaceff9bcfeccd2d4e899195f578e8dac041 Mon Sep 17 00:00:00 2001 From: Vishyyy Date: Sun, 5 Apr 2026 23:22:03 +0530 Subject: [PATCH 25/84] Fixing a Closing Window Bug --- plugins/utilities/advanced_party_window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/utilities/advanced_party_window.py b/plugins/utilities/advanced_party_window.py index 6bdfd2e..58b0e9c 100644 --- a/plugins/utilities/advanced_party_window.py +++ b/plugins/utilities/advanced_party_window.py @@ -461,6 +461,7 @@ class ModifiedPartyWindow(bascenev1lib_party.PartyWindow): color=(0.55, 0.73, 0.25), icon=bui.gettexture('menuButton'), iconscale=1.2) + self._menu_popup: PopupMenuWindow | None = None info = bs.get_connection_to_host_info_2() if info != None: @@ -864,7 +865,7 @@ class ModifiedPartyWindow(bascenev1lib_party.PartyWindow): choices.append("hostInfo_Debug") DisChoices.append(_getTransText("Debug_for_Host_Info", isBaLstr=True)) - PopupMenuWindow( + self._menu_popup = PopupMenuWindow( position=self._menu_button.get_screen_space_center(), scale=_get_popup_window_scale(), choices=choices, @@ -891,7 +892,7 @@ class ModifiedPartyWindow(bascenev1lib_party.PartyWindow): except: babase.print_exception() - PopupMenuWindow(position=widget.get_screen_space_center(), + self._menu_popup = PopupMenuWindow(position=widget.get_screen_space_center(), scale=_get_popup_window_scale(), choices=choices, choices_display=choices_display, From 16cb91a80b00841aef795fcd62a89cb97002f94d Mon Sep 17 00:00:00 2001 From: Vishyyy Date: Sun, 5 Apr 2026 23:26:23 +0530 Subject: [PATCH 26/84] Add version 2.1.6 to utilities.json --- plugins/utilities.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 5a24e31..175f674 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -1054,6 +1054,7 @@ } ], "versions": { + "2.1.6": null, "2.1.5": { "api_version": 9, "commit_sha": "a36e3c2", @@ -2528,4 +2529,4 @@ } } } -} \ No newline at end of file +} From 0288823e6c3abc48d57657af7b1026334fa132e1 Mon Sep 17 00:00:00 2001 From: Vishyyy Date: Mon, 6 Apr 2026 00:17:54 +0530 Subject: [PATCH 27/84] Fixing it back --- plugins/utilities.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 175f674..fa29e80 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -1054,7 +1054,6 @@ } ], "versions": { - "2.1.6": null, "2.1.5": { "api_version": 9, "commit_sha": "a36e3c2", From 8fcb89492aee67603c2a000194016891d9407608 Mon Sep 17 00:00:00 2001 From: Vishyyy Date: Mon, 6 Apr 2026 00:18:59 +0530 Subject: [PATCH 28/84] Updating Advanced Party Window --- plugins/utilities/advanced_party_window.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/utilities/advanced_party_window.py b/plugins/utilities/advanced_party_window.py index 58b0e9c..4d8c88c 100644 --- a/plugins/utilities/advanced_party_window.py +++ b/plugins/utilities/advanced_party_window.py @@ -19,6 +19,16 @@ https://bombsquad-community.web.app/mods ''' +plugman = dict( + plugin_name="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"}, + ] + version="2.1.6", +) + # added advanced ID revealer # live ping From 5ebbadb946df101ccb2b611ea91b3989a8d0c0f7 Mon Sep 17 00:00:00 2001 From: Vishyyy Date: Mon, 6 Apr 2026 00:21:15 +0530 Subject: [PATCH 29/84] Correct authors key syntax in plugin dict Fix syntax error in plugin dictionary definition. --- plugins/utilities/advanced_party_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/utilities/advanced_party_window.py b/plugins/utilities/advanced_party_window.py index 4d8c88c..f70930d 100644 --- a/plugins/utilities/advanced_party_window.py +++ b/plugins/utilities/advanced_party_window.py @@ -23,7 +23,7 @@ plugman = dict( plugin_name="advanced_party_window", description="Advanced your party window with lots of feature", external_url="https://www.youtube.com/watch?v=QrES1jQGXF0", - authors": [ + authors: [ {"name": "Mr.Smoothy", "email": "", "discord": "mr.smoothy"}, ] version="2.1.6", From f74738487fbeb2054ee06cfb3a7133ac3e2e0501 Mon Sep 17 00:00:00 2001 From: Vishyyy Date: Mon, 6 Apr 2026 00:23:37 +0530 Subject: [PATCH 30/84] Fix syntax for authors list in advanced_party_window.py --- plugins/utilities/advanced_party_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/utilities/advanced_party_window.py b/plugins/utilities/advanced_party_window.py index f70930d..973ba5a 100644 --- a/plugins/utilities/advanced_party_window.py +++ b/plugins/utilities/advanced_party_window.py @@ -23,7 +23,7 @@ plugman = dict( plugin_name="advanced_party_window", description="Advanced your party window with lots of feature", external_url="https://www.youtube.com/watch?v=QrES1jQGXF0", - authors: [ + authors=[ {"name": "Mr.Smoothy", "email": "", "discord": "mr.smoothy"}, ] version="2.1.6", From 564357affdf65cf46a748726b5e04dd031734a98 Mon Sep 17 00:00:00 2001 From: vishal332008 <76160371+vishal332008@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:54:01 +0000 Subject: [PATCH 31/84] [ci] apply-plugin-metadata-and-formatting --- plugins/utilities/advanced_party_window.py | 72 +++++++++++----------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/plugins/utilities/advanced_party_window.py b/plugins/utilities/advanced_party_window.py index 973ba5a..e313ba0 100644 --- a/plugins/utilities/advanced_party_window.py +++ b/plugins/utilities/advanced_party_window.py @@ -19,6 +19,37 @@ https://bombsquad-community.web.app/mods ''' +import threading +from _thread import start_new_thread +import urllib.parse +import urllib.request +from typing import TYPE_CHECKING, cast +import _babase +import bascenev1 as bs +import bauiv1 as bui +import babase +import time +import math +from dataclasses import dataclass +from bauiv1lib.confirm import ConfirmWindow +from bauiv1lib.colorpicker import ColorPickerExact +from typing import List, Sequence, Optional, Dict, Any, Union +import bauiv1lib.party as bascenev1lib_party +import ssl +import datetime +import base64 +from babase._general import Call +from bauiv1lib.popup import PopupMenuWindow, PopupWindow +from bauiv1lib.account import viewer +import os +import urllib +import copy +import shutil +import sys +import re +import json +import codecs +import traceback plugman = dict( plugin_name="advanced_party_window", description="Advanced your party window with lots of feature", @@ -33,37 +64,6 @@ plugman = dict( # 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' @@ -903,11 +903,11 @@ class ModifiedPartyWindow(bascenev1lib_party.PartyWindow): babase.print_exception() self._menu_popup = PopupMenuWindow(position=widget.get_screen_space_center(), - scale=_get_popup_window_scale(), - choices=choices, - choices_display=choices_display, - current_choice="@ this guy", - delegate=self) + 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" From 8864fc650e2f87314598f34a79ad62b25a01d029 Mon Sep 17 00:00:00 2001 From: vishal332008 <76160371+vishal332008@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:54:02 +0000 Subject: [PATCH 32/84] [ci] apply-version-metadata --- plugins/utilities.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index fa29e80..5a24e31 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2528,4 +2528,4 @@ } } } -} +} \ No newline at end of file From 3aa666ff65eec34912ba6622e925ada4580f6b54 Mon Sep 17 00:00:00 2001 From: Vishyyy Date: Mon, 6 Apr 2026 00:38:19 +0530 Subject: [PATCH 33/84] Add version 2.1.6 to utilities.json --- plugins/utilities.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 5a24e31..175f674 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -1054,6 +1054,7 @@ } ], "versions": { + "2.1.6": null, "2.1.5": { "api_version": 9, "commit_sha": "a36e3c2", @@ -2528,4 +2529,4 @@ } } } -} \ No newline at end of file +} From 6312b16f492a3039b2ae9c95b3829ce9961e2410 Mon Sep 17 00:00:00 2001 From: vishal332008 <76160371+vishal332008@users.noreply.github.com> Date: Sun, 5 Apr 2026 19:08:41 +0000 Subject: [PATCH 34/84] [ci] apply-version-metadata --- plugins/utilities.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 175f674..2b57682 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -1054,7 +1054,12 @@ } ], "versions": { - "2.1.6": null, + "2.1.6": { + "api_version": 9, + "commit_sha": "3aa666f", + "released_on": "05-04-2026", + "md5sum": "d2b66f98b5a24aa14eb6b1ed9de2d5dc" + }, "2.1.5": { "api_version": 9, "commit_sha": "a36e3c2", @@ -2529,4 +2534,4 @@ } } } -} +} \ No newline at end of file From a8999bd8e114d66896ca109a9596f146b25a86a2 Mon Sep 17 00:00:00 2001 From: Vishyyy Date: Mon, 6 Apr 2026 01:14:45 +0530 Subject: [PATCH 35/84] Update version to 2.1.7 in advanced_party_window.py --- plugins/utilities/advanced_party_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/utilities/advanced_party_window.py b/plugins/utilities/advanced_party_window.py index e313ba0..aca17e7 100644 --- a/plugins/utilities/advanced_party_window.py +++ b/plugins/utilities/advanced_party_window.py @@ -56,8 +56,8 @@ plugman = dict( external_url="https://www.youtube.com/watch?v=QrES1jQGXF0", authors=[ {"name": "Mr.Smoothy", "email": "", "discord": "mr.smoothy"}, - ] - version="2.1.6", + ], + version="2.1.7", ) # added advanced ID revealer From c0527f3635aafa11e71d23c06cf5289ab1b02c76 Mon Sep 17 00:00:00 2001 From: Vishyyy Date: Mon, 6 Apr 2026 01:17:38 +0530 Subject: [PATCH 36/84] Add version 2.1.7 to utilities.json --- plugins/utilities.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 2b57682..7dd7f17 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -1054,6 +1054,7 @@ } ], "versions": { + "2.1.7": null, "2.1.6": { "api_version": 9, "commit_sha": "3aa666f", @@ -2534,4 +2535,4 @@ } } } -} \ No newline at end of file +} From 0417dc09e0b5e5f5a23962b490fb113048918690 Mon Sep 17 00:00:00 2001 From: vishal332008 <76160371+vishal332008@users.noreply.github.com> Date: Sun, 5 Apr 2026 19:48:00 +0000 Subject: [PATCH 37/84] [ci] apply-version-metadata --- plugins/utilities.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 7dd7f17..bc40c74 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -1054,7 +1054,12 @@ } ], "versions": { - "2.1.7": null, + "2.1.7": { + "api_version": 9, + "commit_sha": "c0527f3", + "released_on": "05-04-2026", + "md5sum": "442889926c17a0ffc70b924a808d8132" + }, "2.1.6": { "api_version": 9, "commit_sha": "3aa666f", @@ -2535,4 +2540,4 @@ } } } -} +} \ No newline at end of file From e6c5130186a10e11f4a355c1a2aa4f13afa293d2 Mon Sep 17 00:00:00 2001 From: Anas Date: Fri, 1 May 2026 15:07:09 +0100 Subject: [PATCH 38/84] Delete plugins/utilities/powerup_manager.py --- plugins/utilities/powerup_manager.py | 2985 -------------------------- 1 file changed, 2985 deletions(-) delete mode 100644 plugins/utilities/powerup_manager.py diff --git a/plugins/utilities/powerup_manager.py b/plugins/utilities/powerup_manager.py deleted file mode 100644 index 2a81577..0000000 --- a/plugins/utilities/powerup_manager.py +++ /dev/null @@ -1,2985 +0,0 @@ -# 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() From de9ca9ddb2015b815f0d2f06f2841b6a97c686ec Mon Sep 17 00:00:00 2001 From: Anas Date: Fri, 1 May 2026 15:07:38 +0100 Subject: [PATCH 39/84] Delete plugins/utilities/custom_hits.py --- plugins/utilities/custom_hits.py | 149 ------------------------------- 1 file changed, 149 deletions(-) delete mode 100644 plugins/utilities/custom_hits.py diff --git a/plugins/utilities/custom_hits.py b/plugins/utilities/custom_hits.py deleted file mode 100644 index afa7730..0000000 --- a/plugins/utilities/custom_hits.py +++ /dev/null @@ -1,149 +0,0 @@ -# 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.spaz import Spaz - -if TYPE_CHECKING: - pass - -plugman = dict( - plugin_name="custom_hits", - description="when someone hited rainbow text whit effect will appear", - external_url="", - authors=[ - {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, - ], - version="1.0.1", -) - -#################################### - -first_damage = (u'\ue042TRY AGAIN?\ue042', (0, 1, 0), '6') -second_damage = (u'\ue048GOOD!\ue048', (0, 1, 1), '2') -third_damage = (u'\ue041NICE!\ue041', (0.8, 0.4, 1), '3') -fourth_damage = (u'\ue049UPPS\ue049', (1, 1, 0), '4') -five_damage = (u'\ue043DEATH!\ue043', (1, 0, 0), '5') - - -def custom_effects(pos: float, effect: str = None) -> None: - if effect == '1': - bs.emitfx( - position=pos, - count=3, - scale=0.1, - spread=0.1, - chunk_type='rock') - elif effect == '2': - bs.emitfx( - position=pos, - count=70, - scale=2, - spread=0.8, - chunk_type='spark') - elif effect == '3': - bs.emitfx( - position=pos, - count=6, - scale=0.3, - spread=0.4, - chunk_type='splinter') - elif effect == '4': - bs.emitfx( - position=pos, - count=50, - scale=3.0, - spread=0.9, - chunk_type='ice') - elif effect == '5': - bs.emitfx( - position=pos, - count=80, - scale=3.0, - spread=1.5, - chunk_type='metal') - elif effect == '6': - bs.emitfx( - position=pos, - count=30, - scale=1.2, - spread=0.8, - chunk_type='slime') - -#################################### - -# ba_meta export babase.Plugin - - -class CustomHitsPlugin(babase.Plugin): - - def on_punched(self, damage: int) -> None: - pos = self.node.position - - def custom_text(msg: str, color: float) -> None: - text = bs.newnode( - 'text', - attrs={ - 'text': msg, - 'color': color, - 'in_world': True, - 'h_align': 'center', - 'shadow': 0.5, - 'flatness': 1.0}) - bs.animate_array(text, 'position', 3, { - 0.0: (pos[0], pos[1] + 1.2, pos[2]), - 2.0: (pos[0], pos[1] + 1.7, pos[2]) - }) - bs.animate(text, 'opacity', { - 0.8: 1.0, - 2.0: 0.0 - }) - bs.animate(text, 'scale', { - 0: 0, - 0.1: 0.017, - 0.15: 0.014, - 2.0: 0.016 - }) - bs.animate_array(text, 'color', 3, { - 0.0: (0, 0, 0), # Black - 0.2: (2.55, 2.55, 2.55), # White - 0.4: (2, 0, 0), # Red - 0.6: (0, 2.55, 0), # Lime - 0.8: (0, 0, 2.55), # Blue - 1.0: (2.55, 2.55, 0), # Yellow - 1.2: (0, 2.55, 2.55), # Cyan / Aqua - 1.4: (2.55, 0, 2.55), # Magenta / Fuchsia - 1.6: (1.92, 1.92, 1.92), # Silver - 1.8: (1.28, 1.28, 1.28), # Gray - 2.0: (1.28, 0, 0), # Maroon - 2.2: (1.28, 1.28, 0), # Olive - 2.4: (0, 1.28, 0), # Green - 2.6: (1.28, 0, 1.28), # Purple - 2.8: (0, 1.28, 1.28), # Teal - 3.0: (0, 0, 1.28), # Navy - 3.2: (1.5, 0.5, 0), # Orange - 3.4: (1.8, 0.5, 1.6), # Pink - 3.6: (0.5, 0.5, 0.5), # Gray - }, loop=True) - bs.timer(2.0, text.delete) - if damage < 200: - custom_text(first_damage[0], first_damage[1]) - custom_effects(pos, first_damage[2]) - elif damage < 500: - custom_text(second_damage[0], second_damage[1]) - custom_effects(pos, second_damage[2]) - elif damage < 800: - custom_text(third_damage[0], third_damage[1]) - custom_effects(pos, third_damage[2]) - elif damage < 1000: - custom_text(fourth_damage[0], fourth_damage[1]) - custom_effects(pos, fourth_damage[2]) - else: - custom_text(five_damage[0], five_damage[1]) - custom_effects(pos, five_damage[2]) - Spaz.on_punched = on_punched From 1c728633eacdeeea497ab9939f894356b5d169b4 Mon Sep 17 00:00:00 2001 From: Anas Date: Fri, 1 May 2026 15:12:04 +0100 Subject: [PATCH 40/84] Add custom hit effects and messages Implement custom hit effects and messages based on damage thresholds. --- plugins/custom_hits.py | 93 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 plugins/custom_hits.py diff --git a/plugins/custom_hits.py b/plugins/custom_hits.py new file mode 100644 index 0000000..16cbd50 --- /dev/null +++ b/plugins/custom_hits.py @@ -0,0 +1,93 @@ +# 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.spaz import Spaz + +if TYPE_CHECKING: + pass + +plugman = dict( + plugin_name="custom_hits", + description="when someone hited text whit random effect will appear", + external_url="", + authors=[ + {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, + ], + version="1.1.0", +) +#___________________________________________________ + +first_damage = (u'NAH NOOB?', (1, 1, 1), '6') +second_damage = (u'GOOD!', (0, 1, 0), '2') +third_damage = (u'DAMN!', (0, 0, 0),'3') +fourth_damage = (u'SRY NOOB', (1, 1, 0), '4') +five_damage = (u'FAH!!!', (1, 0, 0), '5') + + +def custom_effects(pos: float, effect: str = None) -> None: + if effect == '1': + bs.emitfx(position=pos, count=3, scale=0.1, spread=0.1, chunk_type='rock') + elif effect == '2': + bs.emitfx(position=pos, count=70, scale=2, spread=0.8, chunk_type='spark') + elif effect == '3': + bs.emitfx(position=pos, count=6, scale=0.3, spread=0.4, chunk_type='splinter') + elif effect == '4': + bs.emitfx(position=pos, count=50, scale=3.0, spread=0.9, chunk_type='ice') + elif effect == '5': + bs.emitfx(position=pos, count=80, scale=3.0, spread=1.5, chunk_type='metal') + elif effect == '6': + bs.emitfx(position=pos, count=30, scale=1.2, spread=0.8, chunk_type='slime') + + +def on_punched(self, damage: int) -> None: + pos = self.node.position + + def custom_text(msg: str, color: float) -> None: + text = bs.newnode('text', attrs={ + 'text': msg, + 'color': color, + 'in_world': True, + 'h_align': 'center', + 'shadow': 0.5, + 'flatness': 1.0, + }) + bs.animate_array(text, 'position', 3, { + 0.0: (pos[0], pos[1] + 1.2, pos[2]), + 2.0: (pos[0], pos[1] + 1.7, pos[2]), + }) + bs.animate(text, 'opacity', {0.8: 1.0, 2.0: 0.0}) + bs.animate(text, 'scale', {0: 0, 0.1: 0.017, 0.15: 0.014, 2.0: 0.016}) + bs.timer(2.0, text.delete) + + if damage < 200: + custom_text(first_damage[0], first_damage[1]) + custom_effects(pos, first_damage[2]) + elif damage < 500: + custom_text(second_damage[0], second_damage[1]) + custom_effects(pos, second_damage[2]) + elif damage < 800: + custom_text(third_damage[0], third_damage[1]) + custom_effects(pos, third_damage[2]) + elif damage < 1000: + custom_text(fourth_damage[0], fourth_damage[1]) + custom_effects(pos, fourth_damage[2]) + else: + custom_text(five_damage[0], five_damage[1]) + custom_effects(pos, five_damage[2]) + + +# ba_meta export babase.Plugin +class byATD(babase.Plugin): + """تفعيل ATD Hits — يُستدعى من bootstraping().""" + Spaz.on_punched = on_punched + try: + from bascenev1lib.actor.spazbot import SpazBot + SpazBot.on_punched = on_punched + except Exception: + pass + bs.broadcastmessage(u' || WELCOME || ', color=(0, 1, 0)) From bbd1fe66323f75b4836e39d88f66e7ab4086981f Mon Sep 17 00:00:00 2001 From: Anas Date: Fri, 1 May 2026 15:12:43 +0100 Subject: [PATCH 41/84] Delete plugins/custom_hits.py --- plugins/custom_hits.py | 93 ------------------------------------------ 1 file changed, 93 deletions(-) delete mode 100644 plugins/custom_hits.py diff --git a/plugins/custom_hits.py b/plugins/custom_hits.py deleted file mode 100644 index 16cbd50..0000000 --- a/plugins/custom_hits.py +++ /dev/null @@ -1,93 +0,0 @@ -# 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.spaz import Spaz - -if TYPE_CHECKING: - pass - -plugman = dict( - plugin_name="custom_hits", - description="when someone hited text whit random effect will appear", - external_url="", - authors=[ - {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, - ], - version="1.1.0", -) -#___________________________________________________ - -first_damage = (u'NAH NOOB?', (1, 1, 1), '6') -second_damage = (u'GOOD!', (0, 1, 0), '2') -third_damage = (u'DAMN!', (0, 0, 0),'3') -fourth_damage = (u'SRY NOOB', (1, 1, 0), '4') -five_damage = (u'FAH!!!', (1, 0, 0), '5') - - -def custom_effects(pos: float, effect: str = None) -> None: - if effect == '1': - bs.emitfx(position=pos, count=3, scale=0.1, spread=0.1, chunk_type='rock') - elif effect == '2': - bs.emitfx(position=pos, count=70, scale=2, spread=0.8, chunk_type='spark') - elif effect == '3': - bs.emitfx(position=pos, count=6, scale=0.3, spread=0.4, chunk_type='splinter') - elif effect == '4': - bs.emitfx(position=pos, count=50, scale=3.0, spread=0.9, chunk_type='ice') - elif effect == '5': - bs.emitfx(position=pos, count=80, scale=3.0, spread=1.5, chunk_type='metal') - elif effect == '6': - bs.emitfx(position=pos, count=30, scale=1.2, spread=0.8, chunk_type='slime') - - -def on_punched(self, damage: int) -> None: - pos = self.node.position - - def custom_text(msg: str, color: float) -> None: - text = bs.newnode('text', attrs={ - 'text': msg, - 'color': color, - 'in_world': True, - 'h_align': 'center', - 'shadow': 0.5, - 'flatness': 1.0, - }) - bs.animate_array(text, 'position', 3, { - 0.0: (pos[0], pos[1] + 1.2, pos[2]), - 2.0: (pos[0], pos[1] + 1.7, pos[2]), - }) - bs.animate(text, 'opacity', {0.8: 1.0, 2.0: 0.0}) - bs.animate(text, 'scale', {0: 0, 0.1: 0.017, 0.15: 0.014, 2.0: 0.016}) - bs.timer(2.0, text.delete) - - if damage < 200: - custom_text(first_damage[0], first_damage[1]) - custom_effects(pos, first_damage[2]) - elif damage < 500: - custom_text(second_damage[0], second_damage[1]) - custom_effects(pos, second_damage[2]) - elif damage < 800: - custom_text(third_damage[0], third_damage[1]) - custom_effects(pos, third_damage[2]) - elif damage < 1000: - custom_text(fourth_damage[0], fourth_damage[1]) - custom_effects(pos, fourth_damage[2]) - else: - custom_text(five_damage[0], five_damage[1]) - custom_effects(pos, five_damage[2]) - - -# ba_meta export babase.Plugin -class byATD(babase.Plugin): - """تفعيل ATD Hits — يُستدعى من bootstraping().""" - Spaz.on_punched = on_punched - try: - from bascenev1lib.actor.spazbot import SpazBot - SpazBot.on_punched = on_punched - except Exception: - pass - bs.broadcastmessage(u' || WELCOME || ', color=(0, 1, 0)) From 120a09f3806057e79c2f87a0fe29d279d240816d Mon Sep 17 00:00:00 2001 From: Anas Date: Fri, 1 May 2026 15:13:38 +0100 Subject: [PATCH 42/84] Add custom hits plugin with visual effects This plugin triggers custom visual effects and messages based on the damage taken by a player. It defines various damage thresholds and corresponding effects, enhancing the gameplay experience. --- plugins/utilities/custom_hits.py | 94 ++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 plugins/utilities/custom_hits.py diff --git a/plugins/utilities/custom_hits.py b/plugins/utilities/custom_hits.py new file mode 100644 index 0000000..87fe9de --- /dev/null +++ b/plugins/utilities/custom_hits.py @@ -0,0 +1,94 @@ +# 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.spaz import Spaz + +if TYPE_CHECKING: + pass + +plugman = dict( + plugin_name="custom_hits", + description="when someone hited text whit random effect will appear", + external_url="", + authors=[ + {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, + ], + version="1.1.0", +) +#___________________________________________________ + +first_damage = (u'NAH NOOB?', (1, 1, 1), '6') +second_damage = (u'GOOD!', (0, 1, 0), '2') +third_damage = (u'DAMN!', (0, 0, 0),'3') +fourth_damage = (u'SRY NOOB', (1, 1, 0), '4') +five_damage = (u'FAH!!!', (1, 0, 0), '5') + + +def custom_effects(pos: float, effect: str = None) -> None: + if effect == '1': + bs.emitfx(position=pos, count=3, scale=0.1, spread=0.1, chunk_type='rock') + elif effect == '2': + bs.emitfx(position=pos, count=70, scale=2, spread=0.8, chunk_type='spark') + elif effect == '3': + bs.emitfx(position=pos, count=6, scale=0.3, spread=0.4, chunk_type='splinter') + elif effect == '4': + bs.emitfx(position=pos, count=50, scale=3.0, spread=0.9, chunk_type='ice') + elif effect == '5': + bs.emitfx(position=pos, count=80, scale=3.0, spread=1.5, chunk_type='metal') + elif effect == '6': + bs.emitfx(position=pos, count=30, scale=1.2, spread=0.8, chunk_type='slime') + + +def on_punched(self, damage: int) -> None: + pos = self.node.position + + def custom_text(msg: str, color: float) -> None: + text = bs.newnode('text', attrs={ + 'text': msg, + 'color': color, + 'in_world': True, + 'h_align': 'center', + 'shadow': 0.5, + 'flatness': 1.0, + }) + bs.animate_array(text, 'position', 3, { + 0.0: (pos[0], pos[1] + 1.2, pos[2]), + 2.0: (pos[0], pos[1] + 1.7, pos[2]), + }) + bs.animate(text, 'opacity', {0.8: 1.0, 2.0: 0.0}) + bs.animate(text, 'scale', {0: 0, 0.1: 0.017, 0.15: 0.014, 2.0: 0.016}) + bs.timer(2.0, text.delete) + + if damage < 200: + custom_text(first_damage[0], first_damage[1]) + custom_effects(pos, first_damage[2]) + elif damage < 500: + custom_text(second_damage[0], second_damage[1]) + custom_effects(pos, second_damage[2]) + elif damage < 800: + custom_text(third_damage[0], third_damage[1]) + custom_effects(pos, third_damage[2]) + elif damage < 1000: + custom_text(fourth_damage[0], fourth_damage[1]) + custom_effects(pos, fourth_damage[2]) + else: + custom_text(five_damage[0], five_damage[1]) + custom_effects(pos, five_damage[2]) + + +# ba_meta export babase.Plugin +class byATD(babase.Plugin): + """تفعيل ATD Hits — يُستدعى من bootstraping().""" + Spaz.on_punched = on_punched + try: + from bascenev1lib.actor.spazbot import SpazBot + SpazBot.on_punched = on_punched + except Exception: + pass + bs.broadcastmessage(u' || WELCOME || ', color=(0, 1, 0)) + From 06d11887a4ffef8a80ed9fbc400f63c98974a743 Mon Sep 17 00:00:00 2001 From: Anas Date: Fri, 1 May 2026 15:15:01 +0100 Subject: [PATCH 43/84] Add rank system plugin for player scoring Implement a rank system for players, including score tracking and rank display. --- plugins/utilities/rank_system.py | 142 +++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 plugins/utilities/rank_system.py diff --git a/plugins/utilities/rank_system.py b/plugins/utilities/rank_system.py new file mode 100644 index 0000000..681bcb4 --- /dev/null +++ b/plugins/utilities/rank_system.py @@ -0,0 +1,142 @@ +# ba_meta require api 9 + +import os +import json +import babase +import bascenev1 as bs + +plugman = dict( + plugin_name="rank_system", + description="ranks system for servers or for local players", + external_url="", + authors=[ + {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, + ], + version="1.0.0", +) + +MODS_DIR = os.path.dirname(__file__) +STATS_DIR = os.path.join(MODS_DIR, "stats") +STATS_FILE = os.path.join(STATS_DIR, "ranks.json") + + +class RankSystem: + + def __init__(self): + self.data = {} + self.load() + + def load(self): + if not os.path.exists(STATS_DIR): + os.makedirs(STATS_DIR) + + if os.path.exists(STATS_FILE): + try: + with open(STATS_FILE, "r") as f: + self.data = json.load(f) + except: + self.data = {} + + def save(self): + with open(STATS_FILE, "w") as f: + json.dump(self.data, f, indent=4) + + def add_score(self, account_id, score=1): + if account_id not in self.data: + self.data[account_id] = { + "score": 0, + "rank": 0 + } + + self.data[account_id]["score"] += score + self.update_ranks() + self.save() + + def update_ranks(self): + sorted_players = sorted( + self.data.items(), + key=lambda x: x[1]["score"], + reverse=True + ) + + for i, (aid, p) in enumerate(sorted_players): + p["rank"] = i + 1 + + def get_rank(self, account_id): + return self.data.get(account_id, {}).get("rank") + + +rank_sys = RankSystem() + + +class RankTag: + def __init__(self, node, rank): + + m = bs.newnode( + "math", + owner=node, + attrs={ + "input1": (0, 1.2, 0), + "operation": "add" + } + ) + + node.connectattr("torso_position", m, "input2") + + if rank == 1: + text = "1" + color = (1, 1, 1) + elif rank == 2: + text = "2" + color = (1, 1, 1) + elif rank == 3: + text = "3" + color = (1, 1, 1) + else: + text = f"#{rank}" + color = (1, 1, 1) + + t = bs.newnode( + "text", + owner=node, + attrs={ + "text": text, + "in_world": True, + "color": color, + "scale": 0.01, + "h_align": "center" + } + ) + + m.connectattr("output", t, "position") + + +# 🔌 plugin +# ba_meta export babase.Plugin +class byATD(babase.Plugin): + + def on_app_running(self): + print("Rank System Loaded ") + + import bascenev1._gameactivity as ga + + old_spawn = ga.GameActivity.spawn_player_spaz + + def new_spawn(self, player, *args, **kwargs): + spaz = old_spawn(self, player, *args, **kwargs) + + try: + aid = player.sessionplayer.get_account_id() + + rank_sys.add_score(aid, 1) + + rank = rank_sys.get_rank(aid) + if rank: + RankTag(spaz.node, rank) + + except Exception as e: + print("Rank error:", e) + + return spaz + + ga.GameActivity.spawn_player_spaz = new_spawn From 3876b25f475b96e2b9b2c2910334d750b8f9c000 Mon Sep 17 00:00:00 2001 From: Anas Date: Fri, 1 May 2026 15:22:43 +0100 Subject: [PATCH 44/84] Update print statement from 'Hello' to 'Goodbye' --- plugins/utilities/powerup_manager.py | 2972 ++++++++++++++++++++++++++ 1 file changed, 2972 insertions(+) create mode 100644 plugins/utilities/powerup_manager.py diff --git a/plugins/utilities/powerup_manager.py b/plugins/utilities/powerup_manager.py new file mode 100644 index 0000000..2f21739 --- /dev/null +++ b/plugins/utilities/powerup_manager.py @@ -0,0 +1,2972 @@ +# 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.5, 0.5, 0.5, 0.5), + draw_controller=self.button, + maxwidth=60, + text=(f"powerups manager 1.1.1"), + 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.CallPartial(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.CallPartial(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": ['levelIcon', (1, 1, 1)], + "Action 2": ['settingsIcon', (1, 1, 1)], + "Action 3": ['inventoryIcon', (1, 1, 1)], + "Action 4": ['storeIcon', (1, 1, 1)], + "Action 5": ['advancedIcon', (1, 1, 1)], + "About": ['achievementEmpty', (1, 1, 1)], + } + + if STORE['Buy Firebombs'] and STORE['Buy Option'] and STORE['Buy Percentage']: + self.tabdefs = { + "Action 1": ['levelIcon', (1, 1, 1)], + "Action 2": ['settingsIcon', (1, 1, 1)], + "Action 3": ['inventoryIcon', (1, 1, 1)], + "About": ['achievementEmpty', (1, 1, 1)], + } + + 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, + big=True, + color=(0.5, 0.5, 0.5), + h_align="center", + v_align="center", + text=("powerup manager 1.1.1"), + 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() + + +# ba_meta export babase.Plugin + + +class UltimatePowerupManager(babase.Plugin): +# 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() From 6c8e898913c0106130a575f196bca08dd820860f Mon Sep 17 00:00:00 2001 From: Anas Date: Fri, 1 May 2026 15:27:48 +0100 Subject: [PATCH 45/84] Bump version from 1.0.0 to 1.0.1 --- plugins/utilities/powerup_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/utilities/powerup_manager.py b/plugins/utilities/powerup_manager.py index 2f21739..c1a9582 100644 --- a/plugins/utilities/powerup_manager.py +++ b/plugins/utilities/powerup_manager.py @@ -34,7 +34,7 @@ plugman = dict( authors=[ {"name": "ATD", "email": "anasdhaoidi001@gmail.com", "discord": ""}, ], - version="1.0.0", + version="1.0.1", ) From 023b733c3b374e297d001bbe82fbb7db71381254 Mon Sep 17 00:00:00 2001 From: anasdhaoidi <223119809+anasdhaoidi@users.noreply.github.com> Date: Fri, 1 May 2026 14:28:15 +0000 Subject: [PATCH 46/84] [ci] apply-plugin-metadata-and-formatting --- plugins/utilities.json | 18 +++++++++++++++++- plugins/utilities/custom_hits.py | 9 ++++----- plugins/utilities/powerup_manager.py | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index bc40c74..1a55428 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2387,6 +2387,7 @@ } ], "versions": { + "1.0.1": null, "1.0.0": { "api_version": 9, "commit_sha": "f38a7ac", @@ -2396,7 +2397,7 @@ } }, "custom_hits": { - "description": "when someone hited rainbow text whit effect will appear", + "description": "when someone hited text whit random effect will appear", "external_url": "", "authors": [ { @@ -2406,6 +2407,7 @@ } ], "versions": { + "1.1.0": null, "1.0.1": { "api_version": 9, "commit_sha": "b48a4bd", @@ -2538,6 +2540,20 @@ "md5sum": "f2b1b542cf8cb7b633c5b460966bd26c" } } + }, + "rank_system": { + "description": "ranks system for servers or for local players", + "external_url": "", + "authors": [ + { + "name": "ATD", + "email": "anasdhaoidi001@gmail.com", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } } } } \ No newline at end of file diff --git a/plugins/utilities/custom_hits.py b/plugins/utilities/custom_hits.py index 87fe9de..e6d3e78 100644 --- a/plugins/utilities/custom_hits.py +++ b/plugins/utilities/custom_hits.py @@ -20,13 +20,13 @@ plugman = dict( ], version="1.1.0", ) -#___________________________________________________ +# ___________________________________________________ -first_damage = (u'NAH NOOB?', (1, 1, 1), '6') +first_damage = (u'NAH NOOB?', (1, 1, 1), '6') second_damage = (u'GOOD!', (0, 1, 0), '2') -third_damage = (u'DAMN!', (0, 0, 0),'3') +third_damage = (u'DAMN!', (0, 0, 0), '3') fourth_damage = (u'SRY NOOB', (1, 1, 0), '4') -five_damage = (u'FAH!!!', (1, 0, 0), '5') +five_damage = (u'FAH!!!', (1, 0, 0), '5') def custom_effects(pos: float, effect: str = None) -> None: @@ -91,4 +91,3 @@ class byATD(babase.Plugin): except Exception: pass bs.broadcastmessage(u' || WELCOME || ', color=(0, 1, 0)) - diff --git a/plugins/utilities/powerup_manager.py b/plugins/utilities/powerup_manager.py index c1a9582..2dad733 100644 --- a/plugins/utilities/powerup_manager.py +++ b/plugins/utilities/powerup_manager.py @@ -2947,7 +2947,7 @@ class PowerupManagerWindow(PopupWindow): class UltimatePowerupManager(babase.Plugin): -# ProfileBrowserWindow = NewProfileBrowserWindow + # ProfileBrowserWindow = NewProfileBrowserWindow pupbox.PowerupBoxFactory = NewPowerupBoxFactory pupbox.PowerupBox.__init__ = _pbx_ Bomb.__init__ = _bomb_init From e131859fe67754907db882c7e0328278c78f0ee3 Mon Sep 17 00:00:00 2001 From: anasdhaoidi <223119809+anasdhaoidi@users.noreply.github.com> Date: Fri, 1 May 2026 14:28:17 +0000 Subject: [PATCH 47/84] [ci] apply-version-metadata --- plugins/utilities.json | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 1a55428..036bc89 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2387,7 +2387,12 @@ } ], "versions": { - "1.0.1": null, + "1.0.1": { + "api_version": 9, + "commit_sha": "023b733", + "released_on": "01-05-2026", + "md5sum": "045eb589b2969398f7fcb171216303d1" + }, "1.0.0": { "api_version": 9, "commit_sha": "f38a7ac", @@ -2407,7 +2412,12 @@ } ], "versions": { - "1.1.0": null, + "1.1.0": { + "api_version": 9, + "commit_sha": "023b733", + "released_on": "01-05-2026", + "md5sum": "bc39a552ab7f40a71ce2813f7860a62f" + }, "1.0.1": { "api_version": 9, "commit_sha": "b48a4bd", @@ -2552,7 +2562,12 @@ } ], "versions": { - "1.0.0": null + "1.0.0": { + "api_version": 9, + "commit_sha": "023b733", + "released_on": "01-05-2026", + "md5sum": "487c0520dc833f15942941e0dbc82f1e" + } } } } From 6212108315ea0d5051e1b1f32258871b8f04d188 Mon Sep 17 00:00:00 2001 From: andrejkuroglo8 Date: Sun, 10 May 2026 14:11:32 +0300 Subject: [PATCH 48/84] Add files via upload --- plugins/utilities/glowing_profiles.py | 218 ++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 plugins/utilities/glowing_profiles.py diff --git a/plugins/utilities/glowing_profiles.py b/plugins/utilities/glowing_profiles.py new file mode 100644 index 0000000..71f03b8 --- /dev/null +++ b/plugins/utilities/glowing_profiles.py @@ -0,0 +1,218 @@ +# ba_meta require api 9 +from __future__ import annotations + +from typing import TYPE_CHECKING +import babase +import _babase + +import bascenev1 as bs +from types import FunctionType +import enum + + +if TYPE_CHECKING: + from typing import Type, Callable, Any, Tuple + +plugman = dict( + plugin_name="glowing_profiles", + description="This plugin gives your profile glowlight, just like on some servers, but only offline", + external_url="https://m.youtube.com/watch?v=Jb_dKz99rhY", + authors=[ + {"name": "andrejkuroglo8", "email": "andrejkuroglo8@gmail.com", "discord": "andrewku"}, + ], + version="1.0.0", +) + +def redefine_method(dst: Tuple[Any, str], src: Tuple[Any, str]) -> None: + if hasattr(getattr(*src), '__redefine_type') and getattr(*src).__redefine_type in ( + RedefineFlag.DECORATE_PRE, RedefineFlag.DECORATE_AFTER, RedefineFlag.DECORATE_ADVANCED): + new = getattr(*src) + old = getattr(*dst) + func: Callable + if getattr(*src).__redefine_type == RedefineFlag.DECORATE_AFTER: + def func(*args, **kwargs): + returned = old(*args, **kwargs) + return new(*args, **kwargs, returned=returned) + elif getattr(*src).__redefine_type == RedefineFlag.DECORATE_PRE: + def func(*args, **kwargs): + new(*args, **kwargs) + return old(*args, **kwargs) + else: + def func(*args, **kwargs): + return new(*args, **kwargs, old_function=old) + + setattr(*dst, func) + else: + setattr(*dst, getattr(*src)) + + # Fucking super()! + # dst.__code__ = CodeType( + # src.__code__.co_argcount, + # src.__code__.co_posonlyargcount, + # src.__code__.co_kwonlyargcount, + # src.__code__.co_nlocals, + # src.__code__.co_stacksize, + # src.__code__.co_flags, + # src.__code__.co_code, + # src.__code__.co_consts, + # src.__code__.co_names, + # src.__code__.co_varnames, + # src.__code__.co_filename, + # dst.__code__.co_name, + # src.__code__.co_firstlineno, + # src.__code__.co_lnotab, + # dst.__code__.co_freevars, + # dst.__code__.co_cellvars) + + +def redefine_class_methods(orig_cls: Type[object]) -> Callable[[Any], None]: + """Returns decorator that redefines all class methods + + Parameters: + orig_cls (Type[object]): class that will redefined""" + + def decorator(cls) -> None: + # for method in filter(lambda x: isinstance(getattr(cls, x), FunctionType), dir(cls)): + for method in cls._redefine_methods: + if hasattr(orig_cls, method): + redefine_method((orig_cls, method), (cls, method)) + else: + setattr(orig_cls, method, getattr(cls, method)) + # setattr(getattr(orig_cls, 'self'), '__class__', getattr(cls, method)) # Fucking super()!!!! + + return decorator + +class RedefineFlag(enum.Enum): + REDEFINE = 0 + DECORATE_AFTER = 1 + DECORATE_PRE = 2 + DECORATE_ADVANCED = 3 + DECORATE = DECORATE_AFTER + + +def redefine_flag(*flags: RedefineFlag) -> Callable[[Callable], Callable]: + def decorator(func: Callable) -> Callable: + for flag in flags: + if flag in (RedefineFlag.DECORATE_AFTER, RedefineFlag.REDEFINE, RedefineFlag.DECORATE_PRE, + RedefineFlag.DECORATE_ADVANCED): + func.__redefine_type = flag + return func + + return decorator + +def get_locale(*args): + return "Error" + + +@redefine_class_methods(bs.Chooser) +class Chooser: + _redefine_methods = ('_gcinit', '_get_glowing_colors', 'update_from_profile', + '_getname') + + def _gcinit(self): + if hasattr(self, '_gcinit_done'): + return + self.glow_dict = {} + self._markers = ('"', "'", '^', '%', ';', '`') + self._get_glowing_colors() + self._gcinit_done = True + + @redefine_flag(RedefineFlag.REDEFINE) + def _get_glowing_colors(self): + """Search glowing code among profiles.""" + try: + should_del = [] + for i in self._profilenames: + for m in self._markers: + if i.startswith(m + ','): + code = i.split(',') + self.glow_dict[code[0]] = ( + float(code[1]), + float(code[2]), + int(code[3]), + int(code[4])) + # should_del.append(i) + for i in should_del: + self._profilenames.remove(i) + except Exception as err: + print(err) + ba.screenmessage( + get_locale('init_glowing_code_error'), + color=(1, 0, 0), + clients=[self._player.get_input_device().client_id], + transient=True) + + @redefine_flag(RedefineFlag.DECORATE_ADVANCED) + def _getname(self, full=True, old_function=None): + name = old_function(self, full) + for m in self._markers: + name = name.replace(m, '') + return name + + @redefine_flag(RedefineFlag.DECORATE_ADVANCED) + def update_from_profile(self, old_function): + self._gcinit() + from bascenev1 import _profile + try: + self._profilename = self._profilenames[self._profileindex] + character = self._profiles[self._profilename]['character'] + + if self._profilename[0] in self.glow_dict: + if (character not in self._character_names + and character in _ba.app.spaz_appearances): + self._character_names.append(character) + self._character_index = self._character_names.index(character) + + player_glowing_dict = self.glow_dict[self._profilename[0]] + color_marker = player_glowing_dict[0] + color_marker = max(-999.0, min(color_marker, 50.0)) + + highlight_marker = float(player_glowing_dict[1]) + highlight_marker = max(-999.0, min(highlight_marker, 50.0)) + + stabilize_color = int(player_glowing_dict[2]) > 0 + stabilize_highlight = int(player_glowing_dict[3]) > 0 + self._color, self._highlight = \ + _profile.get_player_profile_colors( + self._profilename, + profiles=self._profiles) + + if stabilize_color: + m = max(self._color) + self._color = list(self._color) + for i in (0, 1, 2): + if self._color[i] == m: + self._color[i] = self._color[i] * color_marker + self._color = tuple(self._color) + else: + self._color = ( + self._color[0] * color_marker, + self._color[1] * color_marker, + self._color[2] * color_marker) + + if not stabilize_highlight: + self._highlight = ( + self._highlight[0] * highlight_marker, + self._highlight[1] * highlight_marker, + self._highlight[2] * highlight_marker) + else: + m = max(self._highlight) + self._highlight = list(self._highlight) + for i in (0, 1, 2): + if self._highlight[i] == m: + self._highlight[i] = \ + self._highlight[i] * highlight_marker + self._highlight = tuple(self._highlight) + else: + old_function(self) + except KeyError: + self.character_index = self._random_character_index + self._color = self._random_color + self._highlight = self._random_highlight + + self._update_icon() + self._update_text() + +# ba_meta export babase.Plugin +class Glowing(babase.Plugin): + pass \ No newline at end of file From bbf36dc4a9fa1e65290728f6563ccff6d57cb12d Mon Sep 17 00:00:00 2001 From: andrejkuroglo8 Date: Sun, 10 May 2026 14:15:10 +0300 Subject: [PATCH 49/84] Update glowing_profiles.py --- plugins/utilities/glowing_profiles.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/utilities/glowing_profiles.py b/plugins/utilities/glowing_profiles.py index 71f03b8..3668363 100644 --- a/plugins/utilities/glowing_profiles.py +++ b/plugins/utilities/glowing_profiles.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import TYPE_CHECKING import babase import _babase - import bascenev1 as bs from types import FunctionType import enum @@ -215,4 +214,4 @@ class Chooser: # ba_meta export babase.Plugin class Glowing(babase.Plugin): - pass \ No newline at end of file + pass From 7c5038263ceed8d2aaf17c54f6f7db0f7231f492 Mon Sep 17 00:00:00 2001 From: andrejkuroglo8 <176418899+andrejkuroglo8@users.noreply.github.com> Date: Sun, 10 May 2026 11:22:18 +0000 Subject: [PATCH 50/84] [ci] apply-plugin-metadata-and-formatting --- plugins/utilities/glowing_profiles.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/utilities/glowing_profiles.py b/plugins/utilities/glowing_profiles.py index 3668363..799e7c8 100644 --- a/plugins/utilities/glowing_profiles.py +++ b/plugins/utilities/glowing_profiles.py @@ -11,7 +11,7 @@ import enum if TYPE_CHECKING: from typing import Type, Callable, Any, Tuple - + plugman = dict( plugin_name="glowing_profiles", description="This plugin gives your profile glowlight, just like on some servers, but only offline", @@ -22,6 +22,7 @@ plugman = dict( version="1.0.0", ) + def redefine_method(dst: Tuple[Any, str], src: Tuple[Any, str]) -> None: if hasattr(getattr(*src), '__redefine_type') and getattr(*src).__redefine_type in ( RedefineFlag.DECORATE_PRE, RedefineFlag.DECORATE_AFTER, RedefineFlag.DECORATE_ADVANCED): @@ -81,6 +82,7 @@ def redefine_class_methods(orig_cls: Type[object]) -> Callable[[Any], None]: return decorator + class RedefineFlag(enum.Enum): REDEFINE = 0 DECORATE_AFTER = 1 @@ -99,6 +101,7 @@ def redefine_flag(*flags: RedefineFlag) -> Callable[[Callable], Callable]: return decorator + def get_locale(*args): return "Error" @@ -213,5 +216,7 @@ class Chooser: self._update_text() # ba_meta export babase.Plugin + + class Glowing(babase.Plugin): - pass + pass From 6a494a44fc66a981e094e4dc7736794c16e447b1 Mon Sep 17 00:00:00 2001 From: andrejkuroglo8 <176418899+andrejkuroglo8@users.noreply.github.com> Date: Sun, 10 May 2026 11:24:43 +0000 Subject: [PATCH 51/84] [ci] apply-plugin-metadata-and-formatting --- plugins/utilities.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/utilities.json b/plugins/utilities.json index 036bc89..685223b 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2569,6 +2569,20 @@ "md5sum": "487c0520dc833f15942941e0dbc82f1e" } } + }, + "glowing_profiles": { + "description": "This plugin gives your profile glowlight, just like on some servers, but only offline", + "external_url": "https://m.youtube.com/watch?v=Jb_dKz99rhY", + "authors": [ + { + "name": "andrejkuroglo8", + "email": "andrejkuroglo8@gmail.com", + "discord": "andrewku" + } + ], + "versions": { + "1.0.0": null + } } } } \ No newline at end of file From 23fbdf5e9db125a573b552267b5d207fab3cd79a Mon Sep 17 00:00:00 2001 From: andrejkuroglo8 <176418899+andrejkuroglo8@users.noreply.github.com> Date: Sun, 10 May 2026 11:24:45 +0000 Subject: [PATCH 52/84] [ci] apply-version-metadata --- plugins/utilities.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 685223b..7f56f32 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2581,7 +2581,12 @@ } ], "versions": { - "1.0.0": null + "1.0.0": { + "api_version": 9, + "commit_sha": "6a494a4", + "released_on": "10-05-2026", + "md5sum": "6346cd7146baf41eef8926dced23fdfe" + } } } } From e6d9c5d71c0adcc1e0048194659ef2aeaecb1116 Mon Sep 17 00:00:00 2001 From: Scriptz1 <141498178+Scriptz1@users.noreply.github.com> Date: Sun, 31 May 2026 20:43:43 -0400 Subject: [PATCH 53/84] Add files via upload --- plugins/minigames/volleypunch.py | 1301 ++++++++++++++++++++++++++++++ 1 file changed, 1301 insertions(+) create mode 100644 plugins/minigames/volleypunch.py diff --git a/plugins/minigames/volleypunch.py b/plugins/minigames/volleypunch.py new file mode 100644 index 0000000..ae1fcb4 --- /dev/null +++ b/plugins/minigames/volleypunch.py @@ -0,0 +1,1301 @@ +# VolleyPunch ? ig i'll name it like that + +# Made by Learn.py +# but this was def inspired by some freaky guy + + +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase, _babase +import math, random +import bascenev1 as bs +import bauiv1 as bui +import math +import time +import babase +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 + + +plugman = dict( + plugin_name="VolleyPunch", + description="Have fun with your friends in this new volley game !", + external_url="https://github.com/Scriptz1/Learn.py-Installer/blob/main/volleypunch.py", + authors=[ + {"name": "Learn.py", "email": "phillipians36@gmail.com", "discord": "Learning .py"}, + ], + version="1.0.0", +) + +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 + + self.touches = 0 + assert activity is not None + assert isinstance(activity, VolleyBallGame) + pmats = [shared.object_material, activity.puck_material] + + # so this is the volleyball we gonna mesh with today + 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.3, + 'mesh_scale': activity.ball_size, + 'body_scale': 1.07, + 'gravity_scale': 1, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + + + + + 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. + # copied that stuff from hockey game ngl + 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 = 'VolleyPunch' + description = 'Punch the Volleyball !' + 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', 4.0), + ], + default=1.0, + ), + bs.FloatSetting( + 'Ball size (m)', + min_value=0.15, + default=0.25, + max_value=0.35, + increment=0.05, + ), + bs.FloatChoiceSetting( + 'Ball Average Speed', + choices=[ + ('Slower', 0.9899), + ('Normal', 1.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', False), + bs.BoolSetting('Enable 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 ['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.one_sound = bs.getsound('announceOne') + self.two_sound = bs.getsound('announceTwo') + self.three_sound = bs.getsound('announceThree') # unused.. yet + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self.puck_mesh = bs.getmesh('shield') + self.puck_tex = bs.gettexture('ouyaUButton') + self._puck_sound = bs.getsound('metalHit') + self.puck_material = bs.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 4))) + 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), + ('modify_part_collision', 'physical', False))) + + # 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._fake_wall_material = bs.Material() #i'll fix this. trust + + self._net_wall_material = bs.Material() + self._net_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_material.add_actions( + conditions=('they_have_material', shared.object_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True), + ('modify_part_collision', + 'friction', -10), + ('call', 'at_connect', self.slow_down))) + + + + + 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.credit_text = bool(settings['Enable Credits']) # i did the freaku credit thing + self._epic_mode = bool(settings['Epic Mode']) + + + # the ball is too fast for the players and the map is soo huge its a good poc - brostos + self.ball_size = float(settings['Ball size (m)']) + self.ball_speed = float(settings['Ball Average Speed']) + + + # 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.s : list = [] + self.w : list = [] + self.n : list = [] + self.last_interaction_time = -9999 + self.smashed = -9999 + self.interactions_timer = None + self.curve = True + self.difficulty = 0.5 + self.smasher = None + self.commentary_text = None + self._text_task = None # Référence pour annuler l'animation en cours + self._clear_timer = None # Référence pour le timer de disparition + self.touches_indicator = None + self.guide = None + self.curving = False + + 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._puck_spawn_pos = (-4, 1 ,0) if random.random() < 0.5 else (4, 1, 0) + self._spawn_puck() + + # Set up the two score regions. + self._score_regions = [] + self.commentary_text = bs.newnode( + 'text', + attrs=dict( + text='', + scale=0.012, + position=(0, 3.1, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1,1,1,0.5), + h_align= 'center', + v_align= 'top', + h_attach= 'center', + v_attach= 'top', + )) + self.touches_indicator = bs.newnode( + 'text', + attrs=dict( + text='', + scale=0.025, + position=(0, 3, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1,1,1,0.5), + h_align= 'center', + v_align= 'top', + h_attach= 'center', + v_attach= 'top', + )) + self.guide = bs.newnode( + 'text', + attrs=dict( + text='PUNCH TO TAP THE BALL \nPICKUP TO CUFF \nBOMB TO GLIDE / DIVE \nJUMP UNDER A MID AIR BALL WITH THE BAR FILLED TO SMASH \nTHE ULT BAR FILLS AS YOU PLAY', + scale=0.005, + position=(0, 2.7, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1,1,1,0.5), + h_align= 'center', + v_align= 'top', + h_attach= 'center', + v_attach= 'top', + )) + bs.animate_array(self.guide, 'color',4, {0: (1,1,1,0.5), 1: (1,1,1,1), 2: (1,1,1,0.5)}, loop=True) + self.interactions_timer = bs.Timer(1 / 120, self.interactions, repeat=True) # time, function, repeat + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (9, 0, 0), + 'scale': (17.3, 0.002, 12), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (-9, 0, 0), + 'scale': (17.3, 0.002, 20), + '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 Learn.py", + 'scale': 0.7, + 'position': (0, 0), + 'shadow': 0.5, + 'flatness': 1.2, + 'color': (1, 1, 1), + 'h_align': 'center', + 'v_attach': 'bottom'}) + + #Create the net and regions. + mat = bs.Material() # spam this whenever u want a material THEN add some properties + mat.add_actions(actions=('modify_part_collision', + 'collide', False)) + net = bs.newnode( + 'prop', + attrs={ + 'body': 'puck', + 'body_scale': 0, + 'position': (0,0,0), + 'mesh_scale': 1, + 'mesh': bs.getmesh('volleyball_net'), + 'color_texture': bs.gettexture('black'), + 'reflection': 'soft', + 'reflection_scale': [1.5], + 'sticky': False, + 'shadow_size': 0.2, + 'gravity_scale': 0, + 'materials': [mat], + }, + ) + + + self.s.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.w.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( + 0.6, 10, 20), 'type': 'box', 'materials': (self._net_wall_material, )}))) + self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( + 0.6, 2.75, 20), 'type': 'box', 'materials': (self._net_material, )}))) + self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, -5.9), 'scale': ( + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material )}))) + self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 5.9), 'scale': ( + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material )}))) + + def slow_down(self): + if not self._puck: + return + puck_node = self._puck.node + mult = 0.88 + v = puck_node.velocity + puck_node.velocity = (v[0] * mult, v[1] * mult, v[2] * mult) + + def write(self, text, color): + """types""" + if hasattr(self, '_timers'): + for t in self._timers: + t = None + self._timers = [] + + if not text: + return + + self.commentary_text.color = color + total_chars = len(text) + + interval = 0.9 / max(total_chars, 1) + + for i in range(1, total_chars + 1): + def show(index=i): + if self.commentary_text.exists(): + self.commentary_text.text = text[:index] + + self._timers.append(bs.Timer(interval * i, show, repeat=False)) + + def start_clear(): + for i in range(total_chars, -1, -1): + def hide(index=i): + if self.commentary_text.exists(): + self.commentary_text.text = text[:index] + + self._timers.append(bs.Timer(0.9 + 3.0 + (0.015 * (total_chars - i)), hide, repeat=False)) + + start_clear() + + def vignette(self, final_color, longer): + """Aesthetic reasons.""" + glb = bs.getactivity().globalsnode + base_color = (0.57,0.57,0.57) + bs.animate_array(glb, 'vignette_outer', 3, {0: base_color, 0.1: (final_color[0] / 1.2, final_color[1] / 1.2, final_color[2] / 1.2), (1 if longer else 0.5): base_color}) + + def interactions(self): + """Some personalized stuff and my deepest secrets. + I don't know why, but I feel SoK may like this.""" + if self._puck is None: + return + + vel = self._puck.node.velocity + # apply the mult + self._puck.node.velocity = ( + vel[0], + vel[1] * self.ball_speed, + vel[2] + ) + + green = bs.gettexture('ouyaOButton') + yellow = bs.gettexture('ouyaYButton') + red = bs.gettexture('ouyaAButton') + blue = bs.gettexture('ouyaUButton') + + # If it passes the middle.. green. + puck_node = self._puck.node + pos = puck_node.position + x_diff = abs(pos[0]) + + if x_diff < 0.25 or self.curve: + puck_node.color_texture = blue + self._puck.touches = 0 + + if pos[1] < 2.6 and (bs.time() - self.last_interaction_time > 0.5): + mult = 0.935 + v = puck_node.velocity + puck_node.velocity = (v[0] * mult, v[1] * mult, v[2] * mult) + + self.save_text = random.choice([ + "dived to the rescue!", + "kept the ball alive!", + "flew in to save the day!", + "is too fast for the floor!", + "bounced it back just in time!", + "kept the dream alive!", + "barely missed the ground!", + "saved the rally!", + "made a clutch save!", + "refused to let it drop!", + "pulled a superman save!", + "made a desperate save!", + "proved the floor is not an option!", + "landed a miracle touch!", + "saved the point!", + "pulled off a heroic dive!", + "stretched to the limit!", + "barely kept it up!", + "showed reflexes of a cat!", + "denied the floor!", + "made a clutch save!", + "is staying in the game!", + "pulled an epic recovery!", + "reacted in the nick of time!", + "won't let the ball drop!", + "is pure hustle!", + "made an impossible save!", + "is defying gravity!", + "made sure the ground stays empty!", + "saved it!", + "pulled off last-second magic!", + "is floating to save it!", + "is too clutch to fail!", + "got the ball back in the air!", + "is denying the dirt!", + "saved it by a hair!", + "is fighting for every ball!", + "is pure energy!", + "never gives up!", + "keeps it in play!", + "saved it barely!", + "kept the floor clean!", + "is pure determination!", + "pulled a miraculous save!", + "denied the game over!", + "is still alive!", + "is keeping the rally going!", + "made an out-of-reach save!", + "made a ninja-like save!", + "bounced back from trouble!"]) + self.hit_text = random.choice([ + "sent it over!", + "cleared the net!", + "powered it across!", + "nailed a deep shot!", + "bounced it into their zone!", + "launched an attack!", + "sent a rocket over!", + "is dominating the net!", + "found the open spot!", + "placed it perfectly!", + "smashed it back!", + "is applying the pressure!", + "sent them scrambling!", + "over the net it goes!", + "hit a beauty!", + "is controlling the court!", + "forced a tough return!", + "keeps the pressure high!", + "landed a sweet strike!", + "sent it deep!", + "is playing aggressively!", + "hit a laser beam!", + "made a solid connection!", + "is taking charge!", + "sent a tricky shot over!", + "is firing back!", + "put it right on target!", + "sent the ball flying!", + "beat the defense!", + "is crushing it!", + "nailed that serve return!", + "showed off some power!", + "sent a high arc over!", + "is making moves!", + "kept them on their toes!", + "delivered a powerful hit!", + "is pushing the pace!", + "found the gap!", + "sent a curveball over!", + "is looking sharp!", + "hit it with precision!", + "made them work for it!", + "is owning the net!", + "sent a heavy strike!", + "is dictating the play!", + "put it out of reach!", + "landed the perfect hit!", + "is on fire!", + "sent a swift strike!", + "is forcing errors!" + ]) + self.control_text = random.choice([ + "is showing off sweet control!", + "has the ball on a string!", + "handles it with grace!", + "shows off some nice touch!", + "keeps it close and tight!", + "is demonstrating perfect poise!", + "has ice in their veins!", + "is keeping it under control!", + "shows a velvet touch!", + "is masterfully handling the ball!", + "keeps the ball glued to them!", + "is displaying total composure!", + "makes it look so easy!", + "is controlling the pace!", + "shows off pure finesse!", + "has the ball perfectly placed!", + "is staying cool under pressure!", + "has incredible ball handling!", + "is maneuvering with ease!", + "keeps the ball within reach!", + "shows off some fancy footwork!", + "is keeping the ball calm!", + "has the softest touch!", + "is dictating the rhythm!", + "keeps it balanced and steady!", + "is showing high-level control!", + "has the situation handled!", + "is playing with surgical precision!", + "keeps the ball in their zone!", + "shows off expert handling!", + "is keeping it smooth!", + "has the ball right where they want it!", + "demonstrates a gentle touch!", + "is keeping the game slow!", + "shows off calm concentration!", + "has everything under control!", + "is moving with grace!", + "keeps the ball softly hovering!", + "shows off true talent!", + "is maintaining perfect flow!", + "has a magical touch!", + "is playing it smart!", + "keeps the ball very close!", + "shows off professional control!", + "is handling the pressure well!", + "has a very steady hand!", + "is keeping it precise!", + "shows off masterful technique!", + "is keeping the ball in check!", + "has the perfect feel for it!" + ]) + if self.touches_indicator is not None and self.touches_indicator.exists(): + self.touches_indicator.position = (self._puck.node.position[0], self._puck.node.position[1] + 0.35, + self._puck.node.position[2]) + else: + self.touches_indicator = bs.newnode( + 'text', + attrs=dict( + text='', + scale=0.025, + position=(0, 3, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) + + + + everything = bs.getnodes() + everyone = [] + closest_distance = 9999 + closest_player = None + + # i won't explain what "curve" is... look by yourself + if not self.curve and not self._puck.scored: + if bs.time() < self.smashed: + random_pos = (random.uniform(0.1, 0.7) + self._puck.node.position[0],self._puck.node.position[1],random.uniform(0.1, 0.7) + self._puck.node.position[2]) + e = bs.newnode( + 'explosion', + attrs={ + 'position': random_pos, + 'color': self.smasher.color, + 'radius': random.uniform(0.1, 0.7), + 'big': False, + } + ) + bs.timer(1, e.delete) + self.smasher.getdelegate(object).percentage /= 1.1 + else: + ball_tex = self._puck.node.color_texture + + texture_to_name = { + green: 'green', + yellow: 'yellow', + red: 'red', + blue: 'blue' + } + + colors = { + 'green': (0.6, 1, 0.6), + 'yellow': (1, 0.3, 0), + 'red': (1, 0, 0), + 'blue': (0.6, 0.6, 1) + } + + color_name = texture_to_name.get(ball_tex) + color = colors.get(color_name) + + e = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': color, + 'radius': 0.1, + 'big': False, + } + ) + indicator_color = (*color, 0.5) + bs.timer(1, e.delete) + self.touches_indicator.color = indicator_color + self.touches_indicator.text = str(3 - self._puck.touches) + + for human in everything: # doesn't make much sense, but I hope u understand I want players nodes. + if human.getnodetype() == "spaz": + everyone.append(human) + + for player_node in everyone: + if not player_node.exists(): # why wouldn't u exist at first + continue + + + distance = math.dist(player_node.position, self._puck.node.position) # not wanting to do that Euclidean thingy + player_node.hold_node = None + player_node.getdelegate(object).impact_scale = 0 + if distance < closest_distance: + closest_player = player_node # him, the closest one + closest_distance = distance # his distance with the volleyball + + good : bool = (self._puck.touches < 3 and + bs.time() - self.last_interaction_time > 0.4 and + not self._puck.scored) + # bs.time() is the current bombsquad time :) bs for bombsquad. + + + delegate = closest_player.getdelegate(object) + now = bs.time() + can_jump = (delegate.last_jump_time_ms > delegate._jump_cooldown and closest_player.position[1] < 1) + + if closest_player.jump_pressed and can_jump: + ppos = closest_player.position + delegate.last_jump_time_ms = int(bs.time() * 1000) + closest_player.handlemessage( + 'kick_back', + ppos[0], + ppos[1], + ppos[2], + 0, + 1, + 0, + 50, # small force + ) + + + punched = now < delegate.punched + pickup = now < delegate.pickup + bombed = now < delegate.bombed + curved = now < delegate.curved + + + if closest_player and closest_distance < 4 and good: # SO if broski is the nearest AND broski is near enough... + player = spaz = delegate + ball = self._puck.node # ball node + you = closest_player.position # already a node + in_air = (you[1] > 0.8) + if closest_distance < (1.35 if not in_air else 1.6) and (closest_player.punch_pressed or punched): # Ball goes where you look but always tries to go to the other side. (tell me if u guys want it to go totally where you're looking at) + self.difficulty += 0.6 / 75 + e = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': closest_player.color, + 'radius': 0.8, + 'big': False, + } + ) + bs.timer(1, e.delete) + self.last_interaction_time = bs.time() + self._puck.touches += 1 + self.curve = False + if self._puck.touches == 2: + spaz.percentage = min(spaz.percentage + 8, 100) + ball.color_texture = yellow + self.two_sound.play() + elif self._puck.touches == 3: + spaz.percentage = min(spaz.percentage + 12, 100) + ball.color_texture = red + self.one_sound.play() + else: + ball.color_texture = green + bs.getsound("impactHard").play() # This is for the sound, you probably didn't know that! + + + + sight = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward)).normalized() * (12 if delegate.percentage < 95 else 30) # Sight factor + speed = 6 if delegate.percentage < 95 else 20 # bar full + + if curved: + start_pos = ball.position + end_pos = (sight[0] * 2, -1, -sight[2]) + duration = 1.1 + fps = 144 + total_steps = int(duration * fps) + curved_time = bs.time() + 0.15 + self.curving = True + + z_offset = -4.0 if sight[0] < 0 else 4.0 + + def move_to_point(step): + if not ball.exists(): return + if ball.position[1] < 1 and bs.time() > curved_time: return + if not self.curving: return + + + t = step / total_steps + + x = start_pos[0] + (end_pos[0] - start_pos[0]) * t + y = start_pos[1] + (end_pos[1] - start_pos[1]) * t + z = start_pos[2] + (end_pos[2] - start_pos[2]) * t + + y += math.sin(t * math.pi) * 3.0 + + # Ajout de la courbe latérale + z += math.sin(t * math.pi) * z_offset + + # --- LIMITE SUR L'AXE Z --- + # On force z à rester entre -5 et 5 + z = max(-5.0, min(5.0, z)) + # -------------------------- + + ball.position = (x, y, z) + + if step < total_steps: + bs.timer(1 / fps, lambda: move_to_point(step + 1)) + + move_to_point(0) + else: + # normal + self.curving = False + if in_air and delegate.percentage > 95: + if closest_player.position[0] < 0: + vel = (12 * 1.6, 0, sight[2]) * speed + else: + vel = (-12 * 1.6, 0, sight[2]) * speed + else: + if closest_player.position[0] < 0: + vel = (4.8 * 1.6, 0, sight[2]) * speed + else: + vel = (-4.8 * 1.6, 0, sight[2]) * speed + + # smth + dist_from_middle = math.dist(you, (0, 2, 0)) + height = (max(dist_from_middle * 2.75, 7) * 0.8) + if in_air and delegate.percentage > 95: + height = -4 * 2 / dist_from_middle + + ball.velocity = (vel[0], height, vel[2]) + if in_air and delegate.percentage > 95: + audio = random.choice(['explosion01','explosion02','explosion03','explosion04','explosion05']) + bs.getsound(audio).play() + self.smashed = bs.time() + 1 + self.smasher = closest_player + self.vignette(closest_player.color, False) + delegate.on_punch_press() + delegate.on_punch_release() + self.write( + f'{player.name.upper()} SMASHED THE BALL !' if random.random() < 0.5 else f'{player.name} hit the ball with all his might !', closest_player.color) + else: + self.write( + f'{player.name} {self.hit_text}', closest_player.color) + + #prettiness here + ppos = you + f = (bs.Vec3(ball.position) - bs.Vec3(you)).normalized() + closest_player.handlemessage( + 'kick_back', + ppos[0], + ppos[1], + ppos[2], + f[0], + f[1], + f[2], + 400, # small force + ) + if closest_distance < 1.35 and (closest_player.pickup_pressed or pickup): # Ball goes straight up in the air. idk what it is called. + self.write( + f'{player.name} {self.control_text}', closest_player.color) + y_diff = abs(self._puck.node.position[1] - closest_player.position[1]) + + if y_diff < 1: + self.difficulty += 0.6 / 75 + self.last_interaction_time = bs.time() + self.curve = False + + + self._puck.touches += 1 + + ball = self._puck.node + if self._puck.touches == 2: + spaz.percentage = min(spaz.percentage + 4, 100) + ball.color_texture = yellow + self.two_sound.play() + elif self._puck.touches == 3: + spaz.percentage = min(spaz.percentage + 6, 100) + ball.color_texture = red + self.one_sound.play() + else: + ball.color_texture = green + + closest_player.handlemessage('celebrate', 400) + bs.getsound("impactMedium").play() + e = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': closest_player.color, + 'radius': 0.5, + 'big': True, + } + ) + bs.timer(1, e.delete) + + direction = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward)).normalized() + ball.velocity = (-you[0] * 0.04, 12.8, direction[2] * 0.1) + if closest_player.bomb_pressed and not delegate.gliding or bombed and not delegate.gliding: # gliding.. variant coming soon + self.write( + f'{player.name} {self.save_text}', closest_player.color) + delegate.gliding = True + ppos = you + f = (bs.Vec3(ball.position) - bs.Vec3(you)).normalized() + distance = math.dist(ball.position, you) + closest_player.handlemessage( + 'kick_back', + ppos[0], + ppos[1], + ppos[2], + f[0], + 0, + f[2], + 1000 - (500/distance), # tiny force + ) + closest_player.handlemessage('knockout', 100) + print(1) + spaz.pickup = now + 0.8 + + bs.getsound("impactHard").play(volume=0.5) + e = bs.newnode( + 'explosion', + attrs={ + 'position': you, + 'color': (0, 0.3, 0.5), + 'radius': 1.1, + 'big': False, + } + ) + bs.timer(1, e.delete) + bs.timer(0.6, lambda: setattr(delegate, 'gliding', False)) + + # variant soon + + + + + + + + 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 + self.curve = True + + 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 + # to not be getting a corrupted score. + if self._puck.scored: + return + E = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': (0,0,0), + 'radius': .1, + 'big': True, + } + ) + bs.timer(1, E.delete) + bs.animate(self.touches_indicator, 'scale', {0: 0, 2.65: 0, 3: 0.025}) + self.touches_indicator.text = '0' + if self.smashed > 0: + self._puck.node.sticky = True + self.smashed = -9999 + + 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: + team.score += 1 + + # Puck Spawn + if team.id == 0: # left side scored + self._puck_spawn_pos = (5, 0.7, 0) + elif team.id == 1: # right side scored + self._puck_spawn_pos = (-5, 0.7, 0) + else: # what the heck + self._puck_spawn_pos = (0, 0.7, 0) + + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) # yay we scored + player.actor.percentage = min(player.actor.percentage + 15, 100) + # dont mind this ;( + scorch = bs.newnode( + 'scorch', + attrs={ + 'position': self._puck.node.position, + 'size': 1.4, + 'color': player.actor.node.color, + 'big': True, + }, + ) + bs.animate(scorch, 'presence', {3.000: 1, 7.000: 0}) + bs.timer(7, scorch.delete) + self.vignette(player.actor.node.color, longer=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + self._cheer_sound.play() + + self._puck.scored = True + + # Kill the puck (it'll respawn itself shortly). + bs.animate(self._puck.node, 'mesh_scale', {0: 0.25, 0.4 : 0}) # Animation ! 0 sec = 0.25 of size in our case + 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.interactions_timer = None + self.end(results=results) + + def on_transition_in(self) -> None: + super().on_transition_in() + + 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) + spaz.bomb_count = 0 # No bombs for yall bahaha + # Now we want to make that your punches are useless too + # But we will need the button to work so let's put the damage to 0 and some custom stuff + spaz._punch_power_scale = 0 + spaz.impact_scale = 0 + spaz.check_timer = 0 + + spaz.pickup = 0 + spaz.punched = 0 + spaz.bombed = 0 + spaz.curved = 0 + + spaz.can_curve = False + + + spaz.last_angle = 0 + spaz.total_rotation = 0.0 + spaz.rotation_direction = 0 # 0: neutre, 1: clockwise, -1: not clockwise + + spaz.gliding = False + spaz.smashed = False + spaz.percentage = 0 + spaz._jump_cooldown = 2000 + spaz.name = spaz.node.name + spaz.bar = bs.newnode( + 'shield', + owner=spaz.node, + attrs={ + 'position': spaz.node.position, + 'radius': .01, + 'color': (0, 0, 0), + } + ) + spaz.node.connectattr('position', spaz.bar, 'position') + spaz.bar.always_show_health_bar = True + spaz.node.name = '' # bahaha but you'll sspazuseful + + def check(): + if spaz.node.exists(): + # Controls part + now = bs.time() + + if spaz.node.punch_pressed: + spaz.punched = now + 0.25 + elif spaz.node.pickup_pressed: + spaz.pickup = now + 0.40 + elif spaz.node.bomb_pressed: + spaz.bombed = now + 0.20 + elif spaz.node.jump_pressed and spaz.percentage > 95: + spaz.punched = now + 0.50 + + if spaz.percentage > 95: + spaz.node.name = 'SMASH' + else: + spaz.node.name = '' + + + spaz.bar.hurt = min(1.0 - float(spaz.percentage) / 100, 1) + + # Curved ball part + + pos = spaz.node.position + fwd = spaz.node.position_forward + + dx = fwd[0] - pos[0] + dz = fwd[2] - pos[2] + + length = math.sqrt(dx * dx + dz * dz) + if length > 0.0001: + dx /= length + dz /= length + + spaz.angle = math.degrees(math.atan2(-dx, -dz)) # trust + + # checks if u turn ! Not in the game yet ! + def check_270_rotation(): + now = bs.time() + current_angle = spaz.angle + + delta = current_angle - spaz.last_angle + if delta > 180: delta -= 360 + if delta < -180: delta += 360 + + current_dir = 1 if delta > 0 else -1 + + if spaz.rotation_direction != 0 and current_dir != spaz.rotation_direction: + spaz.total_rotation = 0 + + spaz.rotation_direction = current_dir + + spaz.total_rotation += abs(delta) + + if spaz.total_rotation >= 250: + spaz.curved = now + 0.40 + spaz.total_rotation = 0 + + spaz.last_angle = current_angle + + + + + spaz.check_timer1 = bs.Timer(1/30, check, repeat=True) + #spaz.check_timer2 = bs.Timer(1/30, check_270_rotation, repeat=True) + + #spaz.node.hold_node = spaz.node ... nevermind + + # By the way when you have a node but want his player just do + # player = node.getdelegate(object) + # i dont know why you'll need it but here it is + + return spaz + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + # Because if we don't.. they won't like our 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, self._spawn_puck) # 2 secs to breathe bahahaha + else: + super().handlemessage(msg) + + + def _spawn_puck(self) -> None: + self._swipsound.play() + self._whistle_sound.play() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) + self.timer = bs.Timer(1 / 144, self.lock_it, repeat=True) # 144 ?.. yes + def lock_it(self): + if self._puck is None: + return + if self.curve: + self._puck.node.position = self._puck_spawn_pos + else: + self._puck.node.shadow_size = min(self._puck.node.position[1] * 0.3, 2) + + +# ba_meta export babase.Plugin +class justlearn(babase.Plugin): + def __init__(self): + self.installer = ModInstaller() + self.installer.run_full_install() + + +class ModInstaller: + def __init__(self) -> None: + self.python_user = _babase.env()["python_directory_user"] + self.files = self.python_user + '/VolleyBallMap/' + self.app_dir = _babase.env()["python_directory_app"] + '/' + self.data_dir = self.app_dir + '../' + self.meshes_dir = self.data_dir + 'meshes/' + self.platform = _babase.app.classic.platform + + def run_full_install(self) -> None: + import os + import urllib.request + import shutil + import babase + + target_path = os.path.join(self.files, 'volleyball_net.bob') + destination_path = os.path.join(self.meshes_dir, 'volleyball_net.bob') + + if os.path.exists(destination_path): + return + + def _do_install(): + try: + if not os.path.exists(self.files): + os.makedirs(self.files) + + url = "https://raw.githubusercontent.com/Scriptz1/Learn.py-Installer/main/volleyball_net.bob" + urllib.request.urlretrieve(url, target_path) + shutil.copy(target_path, destination_path) + + bui.screenmessage("Installation worked !", color=(0, 1, 0.3)) + bui.getsound('ding').play() + + except Exception as e: + print(f"DEBUG ERROR: {e}") + bui.screenmessage("Installation Failed!", color=(1, 0, 0)) + bui.getsound('kronk2').play() + + + bs.apptimer(2.5, _do_install) \ No newline at end of file From d0e936c582b15aafde596085ca08f2ebe7fba183 Mon Sep 17 00:00:00 2001 From: Scriptz1 <141498178+Scriptz1@users.noreply.github.com> Date: Sun, 31 May 2026 20:55:10 -0400 Subject: [PATCH 54/84] Rename volleypunch.py to volley_punch.py --- plugins/minigames/{volleypunch.py => volley_punch.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename plugins/minigames/{volleypunch.py => volley_punch.py} (99%) diff --git a/plugins/minigames/volleypunch.py b/plugins/minigames/volley_punch.py similarity index 99% rename from plugins/minigames/volleypunch.py rename to plugins/minigames/volley_punch.py index ae1fcb4..5b0d665 100644 --- a/plugins/minigames/volleypunch.py +++ b/plugins/minigames/volley_punch.py @@ -1298,4 +1298,4 @@ class ModInstaller: bui.getsound('kronk2').play() - bs.apptimer(2.5, _do_install) \ No newline at end of file + bs.apptimer(2.5, _do_install) From 9a74e81ec751926e13b15c65c7f614abc991032b Mon Sep 17 00:00:00 2001 From: Scriptz1 <141498178+Scriptz1@users.noreply.github.com> Date: Sun, 31 May 2026 20:57:41 -0400 Subject: [PATCH 55/84] Update volley_punch.py --- plugins/minigames/volley_punch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/minigames/volley_punch.py b/plugins/minigames/volley_punch.py index 5b0d665..b747ad9 100644 --- a/plugins/minigames/volley_punch.py +++ b/plugins/minigames/volley_punch.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: plugman = dict( - plugin_name="VolleyPunch", + plugin_name="volley_punch", description="Have fun with your friends in this new volley game !", external_url="https://github.com/Scriptz1/Learn.py-Installer/blob/main/volleypunch.py", authors=[ From 576a9c030453818d0a6aa7671311d05a5f8200ee Mon Sep 17 00:00:00 2001 From: Scriptz1 <141498178+Scriptz1@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:58:03 +0000 Subject: [PATCH 56/84] [ci] apply-plugin-metadata-and-formatting --- plugins/minigames.json | 14 ++ plugins/minigames/volley_punch.py | 250 +++++++++++++++--------------- 2 files changed, 135 insertions(+), 129 deletions(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index 969670f..9f099ca 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1663,6 +1663,20 @@ "md5sum": "33e6b93420cf6115e992aca534d76bfd" } } + }, + "volley_punch": { + "description": "Have fun with your friends in this new volley game !", + "external_url": "https://github.com/Scriptz1/Learn.py-Installer/blob/main/volleypunch.py", + "authors": [ + { + "name": "Learn.py", + "email": "phillipians36@gmail.com", + "discord": "Learning .py" + } + ], + "versions": { + "1.0.0": null + } } } } \ No newline at end of file diff --git a/plugins/minigames/volley_punch.py b/plugins/minigames/volley_punch.py index b747ad9..c19e269 100644 --- a/plugins/minigames/volley_punch.py +++ b/plugins/minigames/volley_punch.py @@ -10,8 +10,10 @@ from __future__ import annotations from typing import TYPE_CHECKING -import babase, _babase -import math, random +import babase +import _babase +import math +import random import bascenev1 as bs import bauiv1 as bui import math @@ -37,6 +39,7 @@ plugman = dict( version="1.0.0", ) + class PuckDiedMessage: """Inform something that a puck has died.""" @@ -78,9 +81,6 @@ class Puck(bs.Actor): 'materials': pmats }) - - - def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.DieMessage): assert self.node @@ -197,7 +197,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self._chant_sound = bs.getsound('crowdChant') self.one_sound = bs.getsound('announceOne') self.two_sound = bs.getsound('announceTwo') - self.three_sound = bs.getsound('announceThree') # unused.. yet + self.three_sound = bs.getsound('announceThree') # unused.. yet self._swipsound = bs.getsound('swip') self._whistle_sound = bs.getsound('refWhistle') self.puck_mesh = bs.getmesh('shield') @@ -243,7 +243,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', self._handle_score))) - self._fake_wall_material = bs.Material() #i'll fix this. trust + self._fake_wall_material = bs.Material() # i'll fix this. trust self._net_wall_material = bs.Material() self._net_material = bs.Material() @@ -263,30 +263,25 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 'friction', -10), ('call', 'at_connect', self.slow_down))) - - - 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.credit_text = bool(settings['Enable Credits']) # i did the freaku credit thing + self.credit_text = bool(settings['Enable Credits']) # i did the freaku credit thing self._epic_mode = bool(settings['Epic Mode']) - # the ball is too fast for the players and the map is soo huge its a good poc - brostos self.ball_size = float(settings['Ball size (m)']) self.ball_speed = float(settings['Ball Average Speed']) - # 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.s : list = [] - self.w : list = [] - self.n : list = [] + self.s: list = [] + self.w: list = [] + self.n: list = [] self.last_interaction_time = -9999 self.smashed = -9999 self.interactions_timer = None @@ -314,58 +309,60 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): super().on_begin() self.setup_standard_time_limit(self._time_limit) - self._puck_spawn_pos = (-4, 1 ,0) if random.random() < 0.5 else (4, 1, 0) + self._puck_spawn_pos = (-4, 1, 0) if random.random() < 0.5 else (4, 1, 0) self._spawn_puck() # Set up the two score regions. self._score_regions = [] self.commentary_text = bs.newnode( - 'text', - attrs=dict( - text='', - scale=0.012, - position=(0, 3.1, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1,1,1,0.5), - h_align= 'center', - v_align= 'top', - h_attach= 'center', - v_attach= 'top', - )) + 'text', + attrs=dict( + text='', + scale=0.012, + position=(0, 3.1, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) self.touches_indicator = bs.newnode( - 'text', - attrs=dict( - text='', - scale=0.025, - position=(0, 3, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1,1,1,0.5), - h_align= 'center', - v_align= 'top', - h_attach= 'center', - v_attach= 'top', - )) + 'text', + attrs=dict( + text='', + scale=0.025, + position=(0, 3, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) self.guide = bs.newnode( - 'text', - attrs=dict( - text='PUNCH TO TAP THE BALL \nPICKUP TO CUFF \nBOMB TO GLIDE / DIVE \nJUMP UNDER A MID AIR BALL WITH THE BAR FILLED TO SMASH \nTHE ULT BAR FILLS AS YOU PLAY', - scale=0.005, - position=(0, 2.7, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1,1,1,0.5), - h_align= 'center', - v_align= 'top', - h_attach= 'center', - v_attach= 'top', - )) - bs.animate_array(self.guide, 'color',4, {0: (1,1,1,0.5), 1: (1,1,1,1), 2: (1,1,1,0.5)}, loop=True) - self.interactions_timer = bs.Timer(1 / 120, self.interactions, repeat=True) # time, function, repeat + 'text', + attrs=dict( + text='PUNCH TO TAP THE BALL \nPICKUP TO CUFF \nBOMB TO GLIDE / DIVE \nJUMP UNDER A MID AIR BALL WITH THE BAR FILLED TO SMASH \nTHE ULT BAR FILLS AS YOU PLAY', + scale=0.005, + position=(0, 2.7, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) + bs.animate_array(self.guide, 'color', 4, {0: (1, 1, 1, 0.5), 1: ( + 1, 1, 1, 1), 2: (1, 1, 1, 0.5)}, loop=True) + self.interactions_timer = bs.Timer( + 1 / 120, self.interactions, repeat=True) # time, function, repeat self._score_regions.append( bs.NodeActor( bs.newnode('region', @@ -397,16 +394,16 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 'h_align': 'center', 'v_attach': 'bottom'}) - #Create the net and regions. - mat = bs.Material() # spam this whenever u want a material THEN add some properties + # Create the net and regions. + mat = bs.Material() # spam this whenever u want a material THEN add some properties mat.add_actions(actions=('modify_part_collision', - 'collide', False)) + 'collide', False)) net = bs.newnode( 'prop', attrs={ 'body': 'puck', 'body_scale': 0, - 'position': (0,0,0), + 'position': (0, 0, 0), 'mesh_scale': 1, 'mesh': bs.getmesh('volleyball_net'), 'color_texture': bs.gettexture('black'), @@ -419,7 +416,6 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): }, ) - self.s.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 2.4, 0), 'scale': ( 0.8, 6, 20), 'type': 'box', 'materials': (self._fake_wall_material, )}))) @@ -428,9 +424,9 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( 0.6, 2.75, 20), 'type': 'box', 'materials': (self._net_material, )}))) self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, -5.9), 'scale': ( - 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material )}))) + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 5.9), 'scale': ( - 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material )}))) + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) def slow_down(self): if not self._puck: @@ -468,15 +464,17 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): if self.commentary_text.exists(): self.commentary_text.text = text[:index] - self._timers.append(bs.Timer(0.9 + 3.0 + (0.015 * (total_chars - i)), hide, repeat=False)) + self._timers.append( + bs.Timer(0.9 + 3.0 + (0.015 * (total_chars - i)), hide, repeat=False)) start_clear() def vignette(self, final_color, longer): """Aesthetic reasons.""" glb = bs.getactivity().globalsnode - base_color = (0.57,0.57,0.57) - bs.animate_array(glb, 'vignette_outer', 3, {0: base_color, 0.1: (final_color[0] / 1.2, final_color[1] / 1.2, final_color[2] / 1.2), (1 if longer else 0.5): base_color}) + base_color = (0.57, 0.57, 0.57) + bs.animate_array(glb, 'vignette_outer', 3, {0: base_color, 0.1: ( + final_color[0] / 1.2, final_color[1] / 1.2, final_color[2] / 1.2), (1 if longer else 0.5): base_color}) def interactions(self): """Some personalized stuff and my deepest secrets. @@ -686,8 +684,6 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): v_attach='top', )) - - everything = bs.getnodes() everyone = [] closest_distance = 9999 @@ -696,7 +692,8 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # i won't explain what "curve" is... look by yourself if not self.curve and not self._puck.scored: if bs.time() < self.smashed: - random_pos = (random.uniform(0.1, 0.7) + self._puck.node.position[0],self._puck.node.position[1],random.uniform(0.1, 0.7) + self._puck.node.position[2]) + random_pos = (random.uniform( + 0.1, 0.7) + self._puck.node.position[0], self._puck.node.position[1], random.uniform(0.1, 0.7) + self._puck.node.position[2]) e = bs.newnode( 'explosion', attrs={ @@ -742,31 +739,31 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.touches_indicator.color = indicator_color self.touches_indicator.text = str(3 - self._puck.touches) - for human in everything: # doesn't make much sense, but I hope u understand I want players nodes. + for human in everything: # doesn't make much sense, but I hope u understand I want players nodes. if human.getnodetype() == "spaz": everyone.append(human) for player_node in everyone: - if not player_node.exists(): # why wouldn't u exist at first + if not player_node.exists(): # why wouldn't u exist at first continue - - distance = math.dist(player_node.position, self._puck.node.position) # not wanting to do that Euclidean thingy + # not wanting to do that Euclidean thingy + distance = math.dist(player_node.position, self._puck.node.position) player_node.hold_node = None player_node.getdelegate(object).impact_scale = 0 if distance < closest_distance: - closest_player = player_node # him, the closest one - closest_distance = distance # his distance with the volleyball + closest_player = player_node # him, the closest one + closest_distance = distance # his distance with the volleyball - good : bool = (self._puck.touches < 3 and - bs.time() - self.last_interaction_time > 0.4 and - not self._puck.scored) + good: bool = (self._puck.touches < 3 and + bs.time() - self.last_interaction_time > 0.4 and + not self._puck.scored) # bs.time() is the current bombsquad time :) bs for bombsquad. - delegate = closest_player.getdelegate(object) now = bs.time() - can_jump = (delegate.last_jump_time_ms > delegate._jump_cooldown and closest_player.position[1] < 1) + can_jump = (delegate.last_jump_time_ms > + delegate._jump_cooldown and closest_player.position[1] < 1) if closest_player.jump_pressed and can_jump: ppos = closest_player.position @@ -782,19 +779,19 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 50, # small force ) - punched = now < delegate.punched pickup = now < delegate.pickup bombed = now < delegate.bombed curved = now < delegate.curved - - if closest_player and closest_distance < 4 and good: # SO if broski is the nearest AND broski is near enough... + # SO if broski is the nearest AND broski is near enough... + if closest_player and closest_distance < 4 and good: player = spaz = delegate ball = self._puck.node # ball node you = closest_player.position # already a node in_air = (you[1] > 0.8) - if closest_distance < (1.35 if not in_air else 1.6) and (closest_player.punch_pressed or punched): # Ball goes where you look but always tries to go to the other side. (tell me if u guys want it to go totally where you're looking at) + # Ball goes where you look but always tries to go to the other side. (tell me if u guys want it to go totally where you're looking at) + if closest_distance < (1.35 if not in_air else 1.6) and (closest_player.punch_pressed or punched): self.difficulty += 0.6 / 75 e = bs.newnode( 'explosion', @@ -819,12 +816,12 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.one_sound.play() else: ball.color_texture = green - bs.getsound("impactHard").play() # This is for the sound, you probably didn't know that! + # This is for the sound, you probably didn't know that! + bs.getsound("impactHard").play() - - - sight = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward)).normalized() * (12 if delegate.percentage < 95 else 30) # Sight factor - speed = 6 if delegate.percentage < 95 else 20 # bar full + sight = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward) + ).normalized() * (12 if delegate.percentage < 95 else 30) # Sight factor + speed = 6 if delegate.percentage < 95 else 20 # bar full if curved: start_pos = ball.position @@ -838,10 +835,12 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): z_offset = -4.0 if sight[0] < 0 else 4.0 def move_to_point(step): - if not ball.exists(): return - if ball.position[1] < 1 and bs.time() > curved_time: return - if not self.curving: return - + if not ball.exists(): + return + if ball.position[1] < 1 and bs.time() > curved_time: + return + if not self.curving: + return t = step / total_steps @@ -887,7 +886,8 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): ball.velocity = (vel[0], height, vel[2]) if in_air and delegate.percentage > 95: - audio = random.choice(['explosion01','explosion02','explosion03','explosion04','explosion05']) + audio = random.choice(['explosion01', 'explosion02', + 'explosion03', 'explosion04', 'explosion05']) bs.getsound(audio).play() self.smashed = bs.time() + 1 self.smasher = closest_player @@ -900,7 +900,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.write( f'{player.name} {self.hit_text}', closest_player.color) - #prettiness here + # prettiness here ppos = you f = (bs.Vec3(ball.position) - bs.Vec3(you)).normalized() closest_player.handlemessage( @@ -913,7 +913,8 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): f[2], 400, # small force ) - if closest_distance < 1.35 and (closest_player.pickup_pressed or pickup): # Ball goes straight up in the air. idk what it is called. + # Ball goes straight up in the air. idk what it is called. + if closest_distance < 1.35 and (closest_player.pickup_pressed or pickup): self.write( f'{player.name} {self.control_text}', closest_player.color) y_diff = abs(self._puck.node.position[1] - closest_player.position[1]) @@ -923,7 +924,6 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.last_interaction_time = bs.time() self.curve = False - self._puck.touches += 1 ball = self._puck.node @@ -951,9 +951,10 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): ) bs.timer(1, e.delete) - direction = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward)).normalized() + direction = (bs.Vec3(you) - + bs.Vec3(closest_player.position_forward)).normalized() ball.velocity = (-you[0] * 0.04, 12.8, direction[2] * 0.1) - if closest_player.bomb_pressed and not delegate.gliding or bombed and not delegate.gliding: # gliding.. variant coming soon + if closest_player.bomb_pressed and not delegate.gliding or bombed and not delegate.gliding: # gliding.. variant coming soon self.write( f'{player.name} {self.save_text}', closest_player.color) delegate.gliding = True @@ -989,12 +990,6 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # variant soon - - - - - - def on_team_join(self, team: Team) -> None: self._update_scoreboard() @@ -1027,7 +1022,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 'explosion', attrs={ 'position': self._puck.node.position, - 'color': (0,0,0), + 'color': (0, 0, 0), 'radius': .1, 'big': True, } @@ -1059,7 +1054,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): for player in team.players: if player.actor: - player.actor.handlemessage(bs.CelebrateMessage(2.0)) # yay we scored + player.actor.handlemessage(bs.CelebrateMessage(2.0)) # yay we scored player.actor.percentage = min(player.actor.percentage + 15, 100) # dont mind this ;( scorch = bs.newnode( @@ -1084,7 +1079,8 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self._puck.scored = True # Kill the puck (it'll respawn itself shortly). - bs.animate(self._puck.node, 'mesh_scale', {0: 0.25, 0.4 : 0}) # Animation ! 0 sec = 0.25 of size in our case + # Animation ! 0 sec = 0.25 of size in our case + bs.animate(self._puck.node, 'mesh_scale', {0: 0.25, 0.4: 0}) bs.timer(0.7, self._kill_puck) bs.cameraflash(duration=7.0) @@ -1108,7 +1104,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # overriding the default character spawning.. def spawn_player(self, player: Player) -> bs.Actor: spaz = self.spawn_player_spaz(player) - spaz.bomb_count = 0 # No bombs for yall bahaha + spaz.bomb_count = 0 # No bombs for yall bahaha # Now we want to make that your punches are useless too # But we will need the button to work so let's put the damage to 0 and some custom stuff spaz._punch_power_scale = 0 @@ -1122,7 +1118,6 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): spaz.can_curve = False - spaz.last_angle = 0 spaz.total_rotation = 0.0 spaz.rotation_direction = 0 # 0: neutre, 1: clockwise, -1: not clockwise @@ -1143,7 +1138,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): ) spaz.node.connectattr('position', spaz.bar, 'position') spaz.bar.always_show_health_bar = True - spaz.node.name = '' # bahaha but you'll sspazuseful + spaz.node.name = '' # bahaha but you'll sspazuseful def check(): if spaz.node.exists(): @@ -1164,7 +1159,6 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): else: spaz.node.name = '' - spaz.bar.hurt = min(1.0 - float(spaz.percentage) / 100, 1) # Curved ball part @@ -1180,7 +1174,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): dx /= length dz /= length - spaz.angle = math.degrees(math.atan2(-dx, -dz)) # trust + spaz.angle = math.degrees(math.atan2(-dx, -dz)) # trust # checks if u turn ! Not in the game yet ! def check_270_rotation(): @@ -1188,8 +1182,10 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): current_angle = spaz.angle delta = current_angle - spaz.last_angle - if delta > 180: delta -= 360 - if delta < -180: delta += 360 + if delta > 180: + delta -= 360 + if delta < -180: + delta += 360 current_dir = 1 if delta > 0 else -1 @@ -1206,13 +1202,10 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): spaz.last_angle = current_angle - - - spaz.check_timer1 = bs.Timer(1/30, check, repeat=True) - #spaz.check_timer2 = bs.Timer(1/30, check_270_rotation, repeat=True) + # spaz.check_timer2 = bs.Timer(1/30, check_270_rotation, repeat=True) - #spaz.node.hold_node = spaz.node ... nevermind + # spaz.node.hold_node = spaz.node ... nevermind # By the way when you have a node but want his player just do # player = node.getdelegate(object) @@ -1232,17 +1225,17 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # Respawn dead pucks. elif isinstance(msg, PuckDiedMessage): if not self.has_ended(): - bs.timer(2, self._spawn_puck) # 2 secs to breathe bahahaha + bs.timer(2, self._spawn_puck) # 2 secs to breathe bahahaha else: super().handlemessage(msg) - def _spawn_puck(self) -> None: self._swipsound.play() self._whistle_sound.play() assert self._puck_spawn_pos is not None self._puck = Puck(position=self._puck_spawn_pos) - self.timer = bs.Timer(1 / 144, self.lock_it, repeat=True) # 144 ?.. yes + self.timer = bs.Timer(1 / 144, self.lock_it, repeat=True) # 144 ?.. yes + def lock_it(self): if self._puck is None: return @@ -1297,5 +1290,4 @@ class ModInstaller: bui.screenmessage("Installation Failed!", color=(1, 0, 0)) bui.getsound('kronk2').play() - bs.apptimer(2.5, _do_install) From e9e1a29feae96954683f7208c8e0b9f880ae7a57 Mon Sep 17 00:00:00 2001 From: Scriptz1 <141498178+Scriptz1@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:58:05 +0000 Subject: [PATCH 57/84] [ci] apply-version-metadata --- plugins/minigames.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index 9f099ca..9ba5818 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1675,7 +1675,12 @@ } ], "versions": { - "1.0.0": null + "1.0.0": { + "api_version": 9, + "commit_sha": "576a9c0", + "released_on": "01-06-2026", + "md5sum": "be998b599896ed0e8dd5f2545fae6173" + } } } } From f6c0fd5fbe5f491fa365cc34d1a3117e562b400c Mon Sep 17 00:00:00 2001 From: Scriptz1 <141498178+Scriptz1@users.noreply.github.com> Date: Sun, 31 May 2026 21:02:13 -0400 Subject: [PATCH 58/84] Volley Punch --- plugins/minigames.json | 19 + plugins/minigames/volley_punch.py | 1293 +++++++++++++++++++++++++++++ 2 files changed, 1312 insertions(+) create mode 100644 plugins/minigames/volley_punch.py diff --git a/plugins/minigames.json b/plugins/minigames.json index 969670f..9ba5818 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1663,6 +1663,25 @@ "md5sum": "33e6b93420cf6115e992aca534d76bfd" } } + }, + "volley_punch": { + "description": "Have fun with your friends in this new volley game !", + "external_url": "https://github.com/Scriptz1/Learn.py-Installer/blob/main/volleypunch.py", + "authors": [ + { + "name": "Learn.py", + "email": "phillipians36@gmail.com", + "discord": "Learning .py" + } + ], + "versions": { + "1.0.0": { + "api_version": 9, + "commit_sha": "576a9c0", + "released_on": "01-06-2026", + "md5sum": "be998b599896ed0e8dd5f2545fae6173" + } + } } } } \ No newline at end of file diff --git a/plugins/minigames/volley_punch.py b/plugins/minigames/volley_punch.py new file mode 100644 index 0000000..c19e269 --- /dev/null +++ b/plugins/minigames/volley_punch.py @@ -0,0 +1,1293 @@ +# VolleyPunch ? ig i'll name it like that + +# Made by Learn.py +# but this was def inspired by some freaky guy + + +# ba_meta require api 9 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import _babase +import math +import random +import bascenev1 as bs +import bauiv1 as bui +import math +import time +import babase +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 + + +plugman = dict( + plugin_name="volley_punch", + description="Have fun with your friends in this new volley game !", + external_url="https://github.com/Scriptz1/Learn.py-Installer/blob/main/volleypunch.py", + authors=[ + {"name": "Learn.py", "email": "phillipians36@gmail.com", "discord": "Learning .py"}, + ], + version="1.0.0", +) + + +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 + + self.touches = 0 + assert activity is not None + assert isinstance(activity, VolleyBallGame) + pmats = [shared.object_material, activity.puck_material] + + # so this is the volleyball we gonna mesh with today + 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.3, + 'mesh_scale': activity.ball_size, + 'body_scale': 1.07, + 'gravity_scale': 1, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + + 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. + # copied that stuff from hockey game ngl + 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 = 'VolleyPunch' + description = 'Punch the Volleyball !' + 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', 4.0), + ], + default=1.0, + ), + bs.FloatSetting( + 'Ball size (m)', + min_value=0.15, + default=0.25, + max_value=0.35, + increment=0.05, + ), + bs.FloatChoiceSetting( + 'Ball Average Speed', + choices=[ + ('Slower', 0.9899), + ('Normal', 1.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', False), + bs.BoolSetting('Enable 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 ['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.one_sound = bs.getsound('announceOne') + self.two_sound = bs.getsound('announceTwo') + self.three_sound = bs.getsound('announceThree') # unused.. yet + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self.puck_mesh = bs.getmesh('shield') + self.puck_tex = bs.gettexture('ouyaUButton') + self._puck_sound = bs.getsound('metalHit') + self.puck_material = bs.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 4))) + 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), + ('modify_part_collision', 'physical', False))) + + # 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._fake_wall_material = bs.Material() # i'll fix this. trust + + self._net_wall_material = bs.Material() + self._net_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_material.add_actions( + conditions=('they_have_material', shared.object_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True), + ('modify_part_collision', + 'friction', -10), + ('call', 'at_connect', self.slow_down))) + + 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.credit_text = bool(settings['Enable Credits']) # i did the freaku credit thing + self._epic_mode = bool(settings['Epic Mode']) + + # the ball is too fast for the players and the map is soo huge its a good poc - brostos + self.ball_size = float(settings['Ball size (m)']) + self.ball_speed = float(settings['Ball Average Speed']) + + # 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.s: list = [] + self.w: list = [] + self.n: list = [] + self.last_interaction_time = -9999 + self.smashed = -9999 + self.interactions_timer = None + self.curve = True + self.difficulty = 0.5 + self.smasher = None + self.commentary_text = None + self._text_task = None # Référence pour annuler l'animation en cours + self._clear_timer = None # Référence pour le timer de disparition + self.touches_indicator = None + self.guide = None + self.curving = False + + 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._puck_spawn_pos = (-4, 1, 0) if random.random() < 0.5 else (4, 1, 0) + self._spawn_puck() + + # Set up the two score regions. + self._score_regions = [] + self.commentary_text = bs.newnode( + 'text', + attrs=dict( + text='', + scale=0.012, + position=(0, 3.1, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) + self.touches_indicator = bs.newnode( + 'text', + attrs=dict( + text='', + scale=0.025, + position=(0, 3, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) + self.guide = bs.newnode( + 'text', + attrs=dict( + text='PUNCH TO TAP THE BALL \nPICKUP TO CUFF \nBOMB TO GLIDE / DIVE \nJUMP UNDER A MID AIR BALL WITH THE BAR FILLED TO SMASH \nTHE ULT BAR FILLS AS YOU PLAY', + scale=0.005, + position=(0, 2.7, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) + bs.animate_array(self.guide, 'color', 4, {0: (1, 1, 1, 0.5), 1: ( + 1, 1, 1, 1), 2: (1, 1, 1, 0.5)}, loop=True) + self.interactions_timer = bs.Timer( + 1 / 120, self.interactions, repeat=True) # time, function, repeat + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (9, 0, 0), + 'scale': (17.3, 0.002, 12), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (-9, 0, 0), + 'scale': (17.3, 0.002, 20), + '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 Learn.py", + 'scale': 0.7, + 'position': (0, 0), + 'shadow': 0.5, + 'flatness': 1.2, + 'color': (1, 1, 1), + 'h_align': 'center', + 'v_attach': 'bottom'}) + + # Create the net and regions. + mat = bs.Material() # spam this whenever u want a material THEN add some properties + mat.add_actions(actions=('modify_part_collision', + 'collide', False)) + net = bs.newnode( + 'prop', + attrs={ + 'body': 'puck', + 'body_scale': 0, + 'position': (0, 0, 0), + 'mesh_scale': 1, + 'mesh': bs.getmesh('volleyball_net'), + 'color_texture': bs.gettexture('black'), + 'reflection': 'soft', + 'reflection_scale': [1.5], + 'sticky': False, + 'shadow_size': 0.2, + 'gravity_scale': 0, + 'materials': [mat], + }, + ) + + self.s.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.w.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( + 0.6, 10, 20), 'type': 'box', 'materials': (self._net_wall_material, )}))) + self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( + 0.6, 2.75, 20), 'type': 'box', 'materials': (self._net_material, )}))) + self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, -5.9), 'scale': ( + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) + self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 5.9), 'scale': ( + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) + + def slow_down(self): + if not self._puck: + return + puck_node = self._puck.node + mult = 0.88 + v = puck_node.velocity + puck_node.velocity = (v[0] * mult, v[1] * mult, v[2] * mult) + + def write(self, text, color): + """types""" + if hasattr(self, '_timers'): + for t in self._timers: + t = None + self._timers = [] + + if not text: + return + + self.commentary_text.color = color + total_chars = len(text) + + interval = 0.9 / max(total_chars, 1) + + for i in range(1, total_chars + 1): + def show(index=i): + if self.commentary_text.exists(): + self.commentary_text.text = text[:index] + + self._timers.append(bs.Timer(interval * i, show, repeat=False)) + + def start_clear(): + for i in range(total_chars, -1, -1): + def hide(index=i): + if self.commentary_text.exists(): + self.commentary_text.text = text[:index] + + self._timers.append( + bs.Timer(0.9 + 3.0 + (0.015 * (total_chars - i)), hide, repeat=False)) + + start_clear() + + def vignette(self, final_color, longer): + """Aesthetic reasons.""" + glb = bs.getactivity().globalsnode + base_color = (0.57, 0.57, 0.57) + bs.animate_array(glb, 'vignette_outer', 3, {0: base_color, 0.1: ( + final_color[0] / 1.2, final_color[1] / 1.2, final_color[2] / 1.2), (1 if longer else 0.5): base_color}) + + def interactions(self): + """Some personalized stuff and my deepest secrets. + I don't know why, but I feel SoK may like this.""" + if self._puck is None: + return + + vel = self._puck.node.velocity + # apply the mult + self._puck.node.velocity = ( + vel[0], + vel[1] * self.ball_speed, + vel[2] + ) + + green = bs.gettexture('ouyaOButton') + yellow = bs.gettexture('ouyaYButton') + red = bs.gettexture('ouyaAButton') + blue = bs.gettexture('ouyaUButton') + + # If it passes the middle.. green. + puck_node = self._puck.node + pos = puck_node.position + x_diff = abs(pos[0]) + + if x_diff < 0.25 or self.curve: + puck_node.color_texture = blue + self._puck.touches = 0 + + if pos[1] < 2.6 and (bs.time() - self.last_interaction_time > 0.5): + mult = 0.935 + v = puck_node.velocity + puck_node.velocity = (v[0] * mult, v[1] * mult, v[2] * mult) + + self.save_text = random.choice([ + "dived to the rescue!", + "kept the ball alive!", + "flew in to save the day!", + "is too fast for the floor!", + "bounced it back just in time!", + "kept the dream alive!", + "barely missed the ground!", + "saved the rally!", + "made a clutch save!", + "refused to let it drop!", + "pulled a superman save!", + "made a desperate save!", + "proved the floor is not an option!", + "landed a miracle touch!", + "saved the point!", + "pulled off a heroic dive!", + "stretched to the limit!", + "barely kept it up!", + "showed reflexes of a cat!", + "denied the floor!", + "made a clutch save!", + "is staying in the game!", + "pulled an epic recovery!", + "reacted in the nick of time!", + "won't let the ball drop!", + "is pure hustle!", + "made an impossible save!", + "is defying gravity!", + "made sure the ground stays empty!", + "saved it!", + "pulled off last-second magic!", + "is floating to save it!", + "is too clutch to fail!", + "got the ball back in the air!", + "is denying the dirt!", + "saved it by a hair!", + "is fighting for every ball!", + "is pure energy!", + "never gives up!", + "keeps it in play!", + "saved it barely!", + "kept the floor clean!", + "is pure determination!", + "pulled a miraculous save!", + "denied the game over!", + "is still alive!", + "is keeping the rally going!", + "made an out-of-reach save!", + "made a ninja-like save!", + "bounced back from trouble!"]) + self.hit_text = random.choice([ + "sent it over!", + "cleared the net!", + "powered it across!", + "nailed a deep shot!", + "bounced it into their zone!", + "launched an attack!", + "sent a rocket over!", + "is dominating the net!", + "found the open spot!", + "placed it perfectly!", + "smashed it back!", + "is applying the pressure!", + "sent them scrambling!", + "over the net it goes!", + "hit a beauty!", + "is controlling the court!", + "forced a tough return!", + "keeps the pressure high!", + "landed a sweet strike!", + "sent it deep!", + "is playing aggressively!", + "hit a laser beam!", + "made a solid connection!", + "is taking charge!", + "sent a tricky shot over!", + "is firing back!", + "put it right on target!", + "sent the ball flying!", + "beat the defense!", + "is crushing it!", + "nailed that serve return!", + "showed off some power!", + "sent a high arc over!", + "is making moves!", + "kept them on their toes!", + "delivered a powerful hit!", + "is pushing the pace!", + "found the gap!", + "sent a curveball over!", + "is looking sharp!", + "hit it with precision!", + "made them work for it!", + "is owning the net!", + "sent a heavy strike!", + "is dictating the play!", + "put it out of reach!", + "landed the perfect hit!", + "is on fire!", + "sent a swift strike!", + "is forcing errors!" + ]) + self.control_text = random.choice([ + "is showing off sweet control!", + "has the ball on a string!", + "handles it with grace!", + "shows off some nice touch!", + "keeps it close and tight!", + "is demonstrating perfect poise!", + "has ice in their veins!", + "is keeping it under control!", + "shows a velvet touch!", + "is masterfully handling the ball!", + "keeps the ball glued to them!", + "is displaying total composure!", + "makes it look so easy!", + "is controlling the pace!", + "shows off pure finesse!", + "has the ball perfectly placed!", + "is staying cool under pressure!", + "has incredible ball handling!", + "is maneuvering with ease!", + "keeps the ball within reach!", + "shows off some fancy footwork!", + "is keeping the ball calm!", + "has the softest touch!", + "is dictating the rhythm!", + "keeps it balanced and steady!", + "is showing high-level control!", + "has the situation handled!", + "is playing with surgical precision!", + "keeps the ball in their zone!", + "shows off expert handling!", + "is keeping it smooth!", + "has the ball right where they want it!", + "demonstrates a gentle touch!", + "is keeping the game slow!", + "shows off calm concentration!", + "has everything under control!", + "is moving with grace!", + "keeps the ball softly hovering!", + "shows off true talent!", + "is maintaining perfect flow!", + "has a magical touch!", + "is playing it smart!", + "keeps the ball very close!", + "shows off professional control!", + "is handling the pressure well!", + "has a very steady hand!", + "is keeping it precise!", + "shows off masterful technique!", + "is keeping the ball in check!", + "has the perfect feel for it!" + ]) + if self.touches_indicator is not None and self.touches_indicator.exists(): + self.touches_indicator.position = (self._puck.node.position[0], self._puck.node.position[1] + 0.35, + self._puck.node.position[2]) + else: + self.touches_indicator = bs.newnode( + 'text', + attrs=dict( + text='', + scale=0.025, + position=(0, 3, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) + + everything = bs.getnodes() + everyone = [] + closest_distance = 9999 + closest_player = None + + # i won't explain what "curve" is... look by yourself + if not self.curve and not self._puck.scored: + if bs.time() < self.smashed: + random_pos = (random.uniform( + 0.1, 0.7) + self._puck.node.position[0], self._puck.node.position[1], random.uniform(0.1, 0.7) + self._puck.node.position[2]) + e = bs.newnode( + 'explosion', + attrs={ + 'position': random_pos, + 'color': self.smasher.color, + 'radius': random.uniform(0.1, 0.7), + 'big': False, + } + ) + bs.timer(1, e.delete) + self.smasher.getdelegate(object).percentage /= 1.1 + else: + ball_tex = self._puck.node.color_texture + + texture_to_name = { + green: 'green', + yellow: 'yellow', + red: 'red', + blue: 'blue' + } + + colors = { + 'green': (0.6, 1, 0.6), + 'yellow': (1, 0.3, 0), + 'red': (1, 0, 0), + 'blue': (0.6, 0.6, 1) + } + + color_name = texture_to_name.get(ball_tex) + color = colors.get(color_name) + + e = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': color, + 'radius': 0.1, + 'big': False, + } + ) + indicator_color = (*color, 0.5) + bs.timer(1, e.delete) + self.touches_indicator.color = indicator_color + self.touches_indicator.text = str(3 - self._puck.touches) + + for human in everything: # doesn't make much sense, but I hope u understand I want players nodes. + if human.getnodetype() == "spaz": + everyone.append(human) + + for player_node in everyone: + if not player_node.exists(): # why wouldn't u exist at first + continue + + # not wanting to do that Euclidean thingy + distance = math.dist(player_node.position, self._puck.node.position) + player_node.hold_node = None + player_node.getdelegate(object).impact_scale = 0 + if distance < closest_distance: + closest_player = player_node # him, the closest one + closest_distance = distance # his distance with the volleyball + + good: bool = (self._puck.touches < 3 and + bs.time() - self.last_interaction_time > 0.4 and + not self._puck.scored) + # bs.time() is the current bombsquad time :) bs for bombsquad. + + delegate = closest_player.getdelegate(object) + now = bs.time() + can_jump = (delegate.last_jump_time_ms > + delegate._jump_cooldown and closest_player.position[1] < 1) + + if closest_player.jump_pressed and can_jump: + ppos = closest_player.position + delegate.last_jump_time_ms = int(bs.time() * 1000) + closest_player.handlemessage( + 'kick_back', + ppos[0], + ppos[1], + ppos[2], + 0, + 1, + 0, + 50, # small force + ) + + punched = now < delegate.punched + pickup = now < delegate.pickup + bombed = now < delegate.bombed + curved = now < delegate.curved + + # SO if broski is the nearest AND broski is near enough... + if closest_player and closest_distance < 4 and good: + player = spaz = delegate + ball = self._puck.node # ball node + you = closest_player.position # already a node + in_air = (you[1] > 0.8) + # Ball goes where you look but always tries to go to the other side. (tell me if u guys want it to go totally where you're looking at) + if closest_distance < (1.35 if not in_air else 1.6) and (closest_player.punch_pressed or punched): + self.difficulty += 0.6 / 75 + e = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': closest_player.color, + 'radius': 0.8, + 'big': False, + } + ) + bs.timer(1, e.delete) + self.last_interaction_time = bs.time() + self._puck.touches += 1 + self.curve = False + if self._puck.touches == 2: + spaz.percentage = min(spaz.percentage + 8, 100) + ball.color_texture = yellow + self.two_sound.play() + elif self._puck.touches == 3: + spaz.percentage = min(spaz.percentage + 12, 100) + ball.color_texture = red + self.one_sound.play() + else: + ball.color_texture = green + # This is for the sound, you probably didn't know that! + bs.getsound("impactHard").play() + + sight = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward) + ).normalized() * (12 if delegate.percentage < 95 else 30) # Sight factor + speed = 6 if delegate.percentage < 95 else 20 # bar full + + if curved: + start_pos = ball.position + end_pos = (sight[0] * 2, -1, -sight[2]) + duration = 1.1 + fps = 144 + total_steps = int(duration * fps) + curved_time = bs.time() + 0.15 + self.curving = True + + z_offset = -4.0 if sight[0] < 0 else 4.0 + + def move_to_point(step): + if not ball.exists(): + return + if ball.position[1] < 1 and bs.time() > curved_time: + return + if not self.curving: + return + + t = step / total_steps + + x = start_pos[0] + (end_pos[0] - start_pos[0]) * t + y = start_pos[1] + (end_pos[1] - start_pos[1]) * t + z = start_pos[2] + (end_pos[2] - start_pos[2]) * t + + y += math.sin(t * math.pi) * 3.0 + + # Ajout de la courbe latérale + z += math.sin(t * math.pi) * z_offset + + # --- LIMITE SUR L'AXE Z --- + # On force z à rester entre -5 et 5 + z = max(-5.0, min(5.0, z)) + # -------------------------- + + ball.position = (x, y, z) + + if step < total_steps: + bs.timer(1 / fps, lambda: move_to_point(step + 1)) + + move_to_point(0) + else: + # normal + self.curving = False + if in_air and delegate.percentage > 95: + if closest_player.position[0] < 0: + vel = (12 * 1.6, 0, sight[2]) * speed + else: + vel = (-12 * 1.6, 0, sight[2]) * speed + else: + if closest_player.position[0] < 0: + vel = (4.8 * 1.6, 0, sight[2]) * speed + else: + vel = (-4.8 * 1.6, 0, sight[2]) * speed + + # smth + dist_from_middle = math.dist(you, (0, 2, 0)) + height = (max(dist_from_middle * 2.75, 7) * 0.8) + if in_air and delegate.percentage > 95: + height = -4 * 2 / dist_from_middle + + ball.velocity = (vel[0], height, vel[2]) + if in_air and delegate.percentage > 95: + audio = random.choice(['explosion01', 'explosion02', + 'explosion03', 'explosion04', 'explosion05']) + bs.getsound(audio).play() + self.smashed = bs.time() + 1 + self.smasher = closest_player + self.vignette(closest_player.color, False) + delegate.on_punch_press() + delegate.on_punch_release() + self.write( + f'{player.name.upper()} SMASHED THE BALL !' if random.random() < 0.5 else f'{player.name} hit the ball with all his might !', closest_player.color) + else: + self.write( + f'{player.name} {self.hit_text}', closest_player.color) + + # prettiness here + ppos = you + f = (bs.Vec3(ball.position) - bs.Vec3(you)).normalized() + closest_player.handlemessage( + 'kick_back', + ppos[0], + ppos[1], + ppos[2], + f[0], + f[1], + f[2], + 400, # small force + ) + # Ball goes straight up in the air. idk what it is called. + if closest_distance < 1.35 and (closest_player.pickup_pressed or pickup): + self.write( + f'{player.name} {self.control_text}', closest_player.color) + y_diff = abs(self._puck.node.position[1] - closest_player.position[1]) + + if y_diff < 1: + self.difficulty += 0.6 / 75 + self.last_interaction_time = bs.time() + self.curve = False + + self._puck.touches += 1 + + ball = self._puck.node + if self._puck.touches == 2: + spaz.percentage = min(spaz.percentage + 4, 100) + ball.color_texture = yellow + self.two_sound.play() + elif self._puck.touches == 3: + spaz.percentage = min(spaz.percentage + 6, 100) + ball.color_texture = red + self.one_sound.play() + else: + ball.color_texture = green + + closest_player.handlemessage('celebrate', 400) + bs.getsound("impactMedium").play() + e = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': closest_player.color, + 'radius': 0.5, + 'big': True, + } + ) + bs.timer(1, e.delete) + + direction = (bs.Vec3(you) - + bs.Vec3(closest_player.position_forward)).normalized() + ball.velocity = (-you[0] * 0.04, 12.8, direction[2] * 0.1) + if closest_player.bomb_pressed and not delegate.gliding or bombed and not delegate.gliding: # gliding.. variant coming soon + self.write( + f'{player.name} {self.save_text}', closest_player.color) + delegate.gliding = True + ppos = you + f = (bs.Vec3(ball.position) - bs.Vec3(you)).normalized() + distance = math.dist(ball.position, you) + closest_player.handlemessage( + 'kick_back', + ppos[0], + ppos[1], + ppos[2], + f[0], + 0, + f[2], + 1000 - (500/distance), # tiny force + ) + closest_player.handlemessage('knockout', 100) + print(1) + spaz.pickup = now + 0.8 + + bs.getsound("impactHard").play(volume=0.5) + e = bs.newnode( + 'explosion', + attrs={ + 'position': you, + 'color': (0, 0.3, 0.5), + 'radius': 1.1, + 'big': False, + } + ) + bs.timer(1, e.delete) + bs.timer(0.6, lambda: setattr(delegate, 'gliding', False)) + + # variant soon + + 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 + self.curve = True + + 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 + # to not be getting a corrupted score. + if self._puck.scored: + return + E = bs.newnode( + 'explosion', + attrs={ + 'position': self._puck.node.position, + 'color': (0, 0, 0), + 'radius': .1, + 'big': True, + } + ) + bs.timer(1, E.delete) + bs.animate(self.touches_indicator, 'scale', {0: 0, 2.65: 0, 3: 0.025}) + self.touches_indicator.text = '0' + if self.smashed > 0: + self._puck.node.sticky = True + self.smashed = -9999 + + 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: + team.score += 1 + + # Puck Spawn + if team.id == 0: # left side scored + self._puck_spawn_pos = (5, 0.7, 0) + elif team.id == 1: # right side scored + self._puck_spawn_pos = (-5, 0.7, 0) + else: # what the heck + self._puck_spawn_pos = (0, 0.7, 0) + + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) # yay we scored + player.actor.percentage = min(player.actor.percentage + 15, 100) + # dont mind this ;( + scorch = bs.newnode( + 'scorch', + attrs={ + 'position': self._puck.node.position, + 'size': 1.4, + 'color': player.actor.node.color, + 'big': True, + }, + ) + bs.animate(scorch, 'presence', {3.000: 1, 7.000: 0}) + bs.timer(7, scorch.delete) + self.vignette(player.actor.node.color, longer=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + self._cheer_sound.play() + + self._puck.scored = True + + # Kill the puck (it'll respawn itself shortly). + # Animation ! 0 sec = 0.25 of size in our case + bs.animate(self._puck.node, 'mesh_scale', {0: 0.25, 0.4: 0}) + 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.interactions_timer = None + self.end(results=results) + + def on_transition_in(self) -> None: + super().on_transition_in() + + 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) + spaz.bomb_count = 0 # No bombs for yall bahaha + # Now we want to make that your punches are useless too + # But we will need the button to work so let's put the damage to 0 and some custom stuff + spaz._punch_power_scale = 0 + spaz.impact_scale = 0 + spaz.check_timer = 0 + + spaz.pickup = 0 + spaz.punched = 0 + spaz.bombed = 0 + spaz.curved = 0 + + spaz.can_curve = False + + spaz.last_angle = 0 + spaz.total_rotation = 0.0 + spaz.rotation_direction = 0 # 0: neutre, 1: clockwise, -1: not clockwise + + spaz.gliding = False + spaz.smashed = False + spaz.percentage = 0 + spaz._jump_cooldown = 2000 + spaz.name = spaz.node.name + spaz.bar = bs.newnode( + 'shield', + owner=spaz.node, + attrs={ + 'position': spaz.node.position, + 'radius': .01, + 'color': (0, 0, 0), + } + ) + spaz.node.connectattr('position', spaz.bar, 'position') + spaz.bar.always_show_health_bar = True + spaz.node.name = '' # bahaha but you'll sspazuseful + + def check(): + if spaz.node.exists(): + # Controls part + now = bs.time() + + if spaz.node.punch_pressed: + spaz.punched = now + 0.25 + elif spaz.node.pickup_pressed: + spaz.pickup = now + 0.40 + elif spaz.node.bomb_pressed: + spaz.bombed = now + 0.20 + elif spaz.node.jump_pressed and spaz.percentage > 95: + spaz.punched = now + 0.50 + + if spaz.percentage > 95: + spaz.node.name = 'SMASH' + else: + spaz.node.name = '' + + spaz.bar.hurt = min(1.0 - float(spaz.percentage) / 100, 1) + + # Curved ball part + + pos = spaz.node.position + fwd = spaz.node.position_forward + + dx = fwd[0] - pos[0] + dz = fwd[2] - pos[2] + + length = math.sqrt(dx * dx + dz * dz) + if length > 0.0001: + dx /= length + dz /= length + + spaz.angle = math.degrees(math.atan2(-dx, -dz)) # trust + + # checks if u turn ! Not in the game yet ! + def check_270_rotation(): + now = bs.time() + current_angle = spaz.angle + + delta = current_angle - spaz.last_angle + if delta > 180: + delta -= 360 + if delta < -180: + delta += 360 + + current_dir = 1 if delta > 0 else -1 + + if spaz.rotation_direction != 0 and current_dir != spaz.rotation_direction: + spaz.total_rotation = 0 + + spaz.rotation_direction = current_dir + + spaz.total_rotation += abs(delta) + + if spaz.total_rotation >= 250: + spaz.curved = now + 0.40 + spaz.total_rotation = 0 + + spaz.last_angle = current_angle + + spaz.check_timer1 = bs.Timer(1/30, check, repeat=True) + # spaz.check_timer2 = bs.Timer(1/30, check_270_rotation, repeat=True) + + # spaz.node.hold_node = spaz.node ... nevermind + + # By the way when you have a node but want his player just do + # player = node.getdelegate(object) + # i dont know why you'll need it but here it is + + return spaz + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + # Because if we don't.. they won't like our 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, self._spawn_puck) # 2 secs to breathe bahahaha + else: + super().handlemessage(msg) + + def _spawn_puck(self) -> None: + self._swipsound.play() + self._whistle_sound.play() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) + self.timer = bs.Timer(1 / 144, self.lock_it, repeat=True) # 144 ?.. yes + + def lock_it(self): + if self._puck is None: + return + if self.curve: + self._puck.node.position = self._puck_spawn_pos + else: + self._puck.node.shadow_size = min(self._puck.node.position[1] * 0.3, 2) + + +# ba_meta export babase.Plugin +class justlearn(babase.Plugin): + def __init__(self): + self.installer = ModInstaller() + self.installer.run_full_install() + + +class ModInstaller: + def __init__(self) -> None: + self.python_user = _babase.env()["python_directory_user"] + self.files = self.python_user + '/VolleyBallMap/' + self.app_dir = _babase.env()["python_directory_app"] + '/' + self.data_dir = self.app_dir + '../' + self.meshes_dir = self.data_dir + 'meshes/' + self.platform = _babase.app.classic.platform + + def run_full_install(self) -> None: + import os + import urllib.request + import shutil + import babase + + target_path = os.path.join(self.files, 'volleyball_net.bob') + destination_path = os.path.join(self.meshes_dir, 'volleyball_net.bob') + + if os.path.exists(destination_path): + return + + def _do_install(): + try: + if not os.path.exists(self.files): + os.makedirs(self.files) + + url = "https://raw.githubusercontent.com/Scriptz1/Learn.py-Installer/main/volleyball_net.bob" + urllib.request.urlretrieve(url, target_path) + shutil.copy(target_path, destination_path) + + bui.screenmessage("Installation worked !", color=(0, 1, 0.3)) + bui.getsound('ding').play() + + except Exception as e: + print(f"DEBUG ERROR: {e}") + bui.screenmessage("Installation Failed!", color=(1, 0, 0)) + bui.getsound('kronk2').play() + + bs.apptimer(2.5, _do_install) From 8fba8c8ab5f9ef3b620ac4a11050a5c6ac77cc0f Mon Sep 17 00:00:00 2001 From: Scriptz1 <141498178+Scriptz1@users.noreply.github.com> Date: Sun, 31 May 2026 21:11:03 -0400 Subject: [PATCH 59/84] Update volley_punch.py --- plugins/minigames/volley_punch.py | 342 ++++++++++++------------------ 1 file changed, 141 insertions(+), 201 deletions(-) diff --git a/plugins/minigames/volley_punch.py b/plugins/minigames/volley_punch.py index c19e269..8c8b0c8 100644 --- a/plugins/minigames/volley_punch.py +++ b/plugins/minigames/volley_punch.py @@ -10,10 +10,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import babase -import _babase -import math -import random +import babase, _babase +import math, random import bascenev1 as bs import bauiv1 as bui import math @@ -30,7 +28,7 @@ if TYPE_CHECKING: plugman = dict( - plugin_name="volley_punch", + plugin_name="VolleyPunch", description="Have fun with your friends in this new volley game !", external_url="https://github.com/Scriptz1/Learn.py-Installer/blob/main/volleypunch.py", authors=[ @@ -39,7 +37,6 @@ plugman = dict( version="1.0.0", ) - class PuckDiedMessage: """Inform something that a puck has died.""" @@ -81,6 +78,9 @@ class Puck(bs.Actor): 'materials': pmats }) + + + def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.DieMessage): assert self.node @@ -197,7 +197,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self._chant_sound = bs.getsound('crowdChant') self.one_sound = bs.getsound('announceOne') self.two_sound = bs.getsound('announceTwo') - self.three_sound = bs.getsound('announceThree') # unused.. yet + self.three_sound = bs.getsound('announceThree') # unused.. yet self._swipsound = bs.getsound('swip') self._whistle_sound = bs.getsound('refWhistle') self.puck_mesh = bs.getmesh('shield') @@ -243,7 +243,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', self._handle_score))) - self._fake_wall_material = bs.Material() # i'll fix this. trust + self._fake_wall_material = bs.Material() #i'll fix this. trust self._net_wall_material = bs.Material() self._net_material = bs.Material() @@ -263,25 +263,30 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 'friction', -10), ('call', 'at_connect', self.slow_down))) + + + 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.credit_text = bool(settings['Enable Credits']) # i did the freaku credit thing + self.credit_text = bool(settings['Enable Credits']) # i did the freaku credit thing self._epic_mode = bool(settings['Epic Mode']) + # the ball is too fast for the players and the map is soo huge its a good poc - brostos self.ball_size = float(settings['Ball size (m)']) self.ball_speed = float(settings['Ball Average Speed']) + # 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.s: list = [] - self.w: list = [] - self.n: list = [] + self.s : list = [] + self.w : list = [] + self.n : list = [] self.last_interaction_time = -9999 self.smashed = -9999 self.interactions_timer = None @@ -309,60 +314,58 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): super().on_begin() self.setup_standard_time_limit(self._time_limit) - self._puck_spawn_pos = (-4, 1, 0) if random.random() < 0.5 else (4, 1, 0) + self._puck_spawn_pos = (-4, 1 ,0) if random.random() < 0.5 else (4, 1, 0) self._spawn_puck() # Set up the two score regions. self._score_regions = [] self.commentary_text = bs.newnode( - 'text', - attrs=dict( - text='', - scale=0.012, - position=(0, 3.1, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1, 1, 1, 0.5), - h_align='center', - v_align='top', - h_attach='center', - v_attach='top', - )) + 'text', + attrs=dict( + text='', + scale=0.012, + position=(0, 3.1, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1,1,1,0.5), + h_align= 'center', + v_align= 'top', + h_attach= 'center', + v_attach= 'top', + )) self.touches_indicator = bs.newnode( - 'text', - attrs=dict( - text='', - scale=0.025, - position=(0, 3, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1, 1, 1, 0.5), - h_align='center', - v_align='top', - h_attach='center', - v_attach='top', - )) + 'text', + attrs=dict( + text='', + scale=0.025, + position=(0, 3, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1,1,1,0.5), + h_align= 'center', + v_align= 'top', + h_attach= 'center', + v_attach= 'top', + )) self.guide = bs.newnode( - 'text', - attrs=dict( - text='PUNCH TO TAP THE BALL \nPICKUP TO CUFF \nBOMB TO GLIDE / DIVE \nJUMP UNDER A MID AIR BALL WITH THE BAR FILLED TO SMASH \nTHE ULT BAR FILLS AS YOU PLAY', - scale=0.005, - position=(0, 2.7, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1, 1, 1, 0.5), - h_align='center', - v_align='top', - h_attach='center', - v_attach='top', - )) - bs.animate_array(self.guide, 'color', 4, {0: (1, 1, 1, 0.5), 1: ( - 1, 1, 1, 1), 2: (1, 1, 1, 0.5)}, loop=True) - self.interactions_timer = bs.Timer( - 1 / 120, self.interactions, repeat=True) # time, function, repeat + 'text', + attrs=dict( + text='PUNCH TO TAP THE BALL \nPICKUP TO CUFF \nBOMB TO GLIDE / DIVE \nJUMP UNDER A MID AIR BALL WITH THE BAR FILLED TO SMASH \nTHE ULT BAR FILLS AS YOU PLAY', + scale=0.005, + position=(0, 2.7, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1,1,1,0.5), + h_align= 'center', + v_align= 'top', + h_attach= 'center', + v_attach= 'top', + )) + bs.animate_array(self.guide, 'color',4, {0: (1,1,1,0.5), 1: (1,1,1,1), 2: (1,1,1,0.5)}, loop=True) + self.interactions_timer = bs.Timer(1 / 120, self.interactions, repeat=True) # time, function, repeat self._score_regions.append( bs.NodeActor( bs.newnode('region', @@ -394,16 +397,16 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 'h_align': 'center', 'v_attach': 'bottom'}) - # Create the net and regions. - mat = bs.Material() # spam this whenever u want a material THEN add some properties + #Create the net and regions. + mat = bs.Material() # spam this whenever u want a material THEN add some properties mat.add_actions(actions=('modify_part_collision', - 'collide', False)) + 'collide', False)) net = bs.newnode( 'prop', attrs={ 'body': 'puck', 'body_scale': 0, - 'position': (0, 0, 0), + 'position': (0,0,0), 'mesh_scale': 1, 'mesh': bs.getmesh('volleyball_net'), 'color_texture': bs.gettexture('black'), @@ -416,6 +419,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): }, ) + self.s.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 2.4, 0), 'scale': ( 0.8, 6, 20), 'type': 'box', 'materials': (self._fake_wall_material, )}))) @@ -424,9 +428,9 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( 0.6, 2.75, 20), 'type': 'box', 'materials': (self._net_material, )}))) self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, -5.9), 'scale': ( - 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material )}))) self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 5.9), 'scale': ( - 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material )}))) def slow_down(self): if not self._puck: @@ -464,17 +468,15 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): if self.commentary_text.exists(): self.commentary_text.text = text[:index] - self._timers.append( - bs.Timer(0.9 + 3.0 + (0.015 * (total_chars - i)), hide, repeat=False)) + self._timers.append(bs.Timer(0.9 + 3.0 + (0.015 * (total_chars - i)), hide, repeat=False)) start_clear() def vignette(self, final_color, longer): """Aesthetic reasons.""" glb = bs.getactivity().globalsnode - base_color = (0.57, 0.57, 0.57) - bs.animate_array(glb, 'vignette_outer', 3, {0: base_color, 0.1: ( - final_color[0] / 1.2, final_color[1] / 1.2, final_color[2] / 1.2), (1 if longer else 0.5): base_color}) + base_color = (0.57,0.57,0.57) + bs.animate_array(glb, 'vignette_outer', 3, {0: base_color, 0.1: (final_color[0] / 1.2, final_color[1] / 1.2, final_color[2] / 1.2), (1 if longer else 0.5): base_color}) def interactions(self): """Some personalized stuff and my deepest secrets. @@ -684,6 +686,8 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): v_attach='top', )) + + everything = bs.getnodes() everyone = [] closest_distance = 9999 @@ -692,8 +696,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # i won't explain what "curve" is... look by yourself if not self.curve and not self._puck.scored: if bs.time() < self.smashed: - random_pos = (random.uniform( - 0.1, 0.7) + self._puck.node.position[0], self._puck.node.position[1], random.uniform(0.1, 0.7) + self._puck.node.position[2]) + random_pos = (random.uniform(0.1, 0.7) + self._puck.node.position[0],self._puck.node.position[1],random.uniform(0.1, 0.7) + self._puck.node.position[2]) e = bs.newnode( 'explosion', attrs={ @@ -739,31 +742,31 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.touches_indicator.color = indicator_color self.touches_indicator.text = str(3 - self._puck.touches) - for human in everything: # doesn't make much sense, but I hope u understand I want players nodes. + for human in everything: # doesn't make much sense, but I hope u understand I want players nodes. if human.getnodetype() == "spaz": everyone.append(human) for player_node in everyone: - if not player_node.exists(): # why wouldn't u exist at first + if not player_node.exists(): # why wouldn't u exist at first continue - # not wanting to do that Euclidean thingy - distance = math.dist(player_node.position, self._puck.node.position) + + distance = math.dist(player_node.position, self._puck.node.position) # not wanting to do that Euclidean thingy player_node.hold_node = None player_node.getdelegate(object).impact_scale = 0 if distance < closest_distance: - closest_player = player_node # him, the closest one - closest_distance = distance # his distance with the volleyball + closest_player = player_node # him, the closest one + closest_distance = distance # his distance with the volleyball - good: bool = (self._puck.touches < 3 and - bs.time() - self.last_interaction_time > 0.4 and - not self._puck.scored) + good : bool = (self._puck.touches < 3 and + bs.time() - self.last_interaction_time > 0.4 and + not self._puck.scored) # bs.time() is the current bombsquad time :) bs for bombsquad. + delegate = closest_player.getdelegate(object) now = bs.time() - can_jump = (delegate.last_jump_time_ms > - delegate._jump_cooldown and closest_player.position[1] < 1) + can_jump = (delegate.last_jump_time_ms > delegate._jump_cooldown and closest_player.position[1] < 1) if closest_player.jump_pressed and can_jump: ppos = closest_player.position @@ -779,19 +782,19 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 50, # small force ) + punched = now < delegate.punched pickup = now < delegate.pickup bombed = now < delegate.bombed curved = now < delegate.curved - # SO if broski is the nearest AND broski is near enough... - if closest_player and closest_distance < 4 and good: + + if closest_player and closest_distance < 4 and good: # SO if broski is the nearest AND broski is near enough... player = spaz = delegate ball = self._puck.node # ball node you = closest_player.position # already a node in_air = (you[1] > 0.8) - # Ball goes where you look but always tries to go to the other side. (tell me if u guys want it to go totally where you're looking at) - if closest_distance < (1.35 if not in_air else 1.6) and (closest_player.punch_pressed or punched): + if closest_distance < (1.35 if not in_air else 1.6) and (closest_player.punch_pressed or punched): # Ball goes where you look but always tries to go to the other side. (tell me if u guys want it to go totally where you're looking at) self.difficulty += 0.6 / 75 e = bs.newnode( 'explosion', @@ -816,78 +819,38 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.one_sound.play() else: ball.color_texture = green - # This is for the sound, you probably didn't know that! - bs.getsound("impactHard").play() + bs.getsound("impactHard").play() # This is for the sound, you probably didn't know that! - sight = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward) - ).normalized() * (12 if delegate.percentage < 95 else 30) # Sight factor - speed = 6 if delegate.percentage < 95 else 20 # bar full - if curved: - start_pos = ball.position - end_pos = (sight[0] * 2, -1, -sight[2]) - duration = 1.1 - fps = 144 - total_steps = int(duration * fps) - curved_time = bs.time() + 0.15 - self.curving = True - z_offset = -4.0 if sight[0] < 0 else 4.0 + sight = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward)).normalized() * (12 if delegate.percentage < 95 else 30) # Sight factor + speed = 6 if delegate.percentage < 95 else 20 # bar full - def move_to_point(step): - if not ball.exists(): - return - if ball.position[1] < 1 and bs.time() > curved_time: - return - if not self.curving: - return - - t = step / total_steps - - x = start_pos[0] + (end_pos[0] - start_pos[0]) * t - y = start_pos[1] + (end_pos[1] - start_pos[1]) * t - z = start_pos[2] + (end_pos[2] - start_pos[2]) * t - - y += math.sin(t * math.pi) * 3.0 - - # Ajout de la courbe latérale - z += math.sin(t * math.pi) * z_offset - - # --- LIMITE SUR L'AXE Z --- - # On force z à rester entre -5 et 5 - z = max(-5.0, min(5.0, z)) - # -------------------------- - - ball.position = (x, y, z) - - if step < total_steps: - bs.timer(1 / fps, lambda: move_to_point(step + 1)) - - move_to_point(0) - else: - # normal - self.curving = False - if in_air and delegate.percentage > 95: - if closest_player.position[0] < 0: - vel = (12 * 1.6, 0, sight[2]) * speed - else: - vel = (-12 * 1.6, 0, sight[2]) * speed - else: - if closest_player.position[0] < 0: - vel = (4.8 * 1.6, 0, sight[2]) * speed - else: - vel = (-4.8 * 1.6, 0, sight[2]) * speed - - # smth - dist_from_middle = math.dist(you, (0, 2, 0)) - height = (max(dist_from_middle * 2.75, 7) * 0.8) - if in_air and delegate.percentage > 95: - height = -4 * 2 / dist_from_middle - - ball.velocity = (vel[0], height, vel[2]) + # normal + self.curving = False if in_air and delegate.percentage > 95: - audio = random.choice(['explosion01', 'explosion02', - 'explosion03', 'explosion04', 'explosion05']) + if closest_player.position[0] < 0: + vel = (12 * 1.6, 0, sight[2]) * speed + else: + vel = (-12 * 1.6, 0, sight[2]) * speed + else: + if closest_player.position[0] < 0: + vel = (4.8 * 1.6, 0, sight[2]) * speed + else: + vel = (-4.8 * 1.6, 0, sight[2]) * speed + + # smth + dist_from_middle = math.dist(you, (0, 2, 0)) + height = (max(dist_from_middle * 2.75, 7) * 0.8) + if in_air and delegate.percentage <= 95: + height = 3.5 + vel = (vel[0] * 1.5, height, vel[2]) + if in_air and delegate.percentage > 95: + height = -4 * 2 / dist_from_middle + + ball.velocity = (vel[0], height, vel[2]) + if in_air and delegate.percentage > 95: + audio = random.choice(['explosion01','explosion02','explosion03','explosion04','explosion05']) bs.getsound(audio).play() self.smashed = bs.time() + 1 self.smasher = closest_player @@ -900,7 +863,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.write( f'{player.name} {self.hit_text}', closest_player.color) - # prettiness here + #prettiness here ppos = you f = (bs.Vec3(ball.position) - bs.Vec3(you)).normalized() closest_player.handlemessage( @@ -913,8 +876,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): f[2], 400, # small force ) - # Ball goes straight up in the air. idk what it is called. - if closest_distance < 1.35 and (closest_player.pickup_pressed or pickup): + if closest_distance < 1.35 and (closest_player.pickup_pressed or pickup): # Ball goes straight up in the air. idk what it is called. self.write( f'{player.name} {self.control_text}', closest_player.color) y_diff = abs(self._puck.node.position[1] - closest_player.position[1]) @@ -924,6 +886,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.last_interaction_time = bs.time() self.curve = False + self._puck.touches += 1 ball = self._puck.node @@ -951,10 +914,9 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): ) bs.timer(1, e.delete) - direction = (bs.Vec3(you) - - bs.Vec3(closest_player.position_forward)).normalized() + direction = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward)).normalized() ball.velocity = (-you[0] * 0.04, 12.8, direction[2] * 0.1) - if closest_player.bomb_pressed and not delegate.gliding or bombed and not delegate.gliding: # gliding.. variant coming soon + if closest_player.bomb_pressed and not delegate.gliding or bombed and not delegate.gliding: # gliding.. variant coming soon self.write( f'{player.name} {self.save_text}', closest_player.color) delegate.gliding = True @@ -990,6 +952,12 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # variant soon + + + + + + def on_team_join(self, team: Team) -> None: self._update_scoreboard() @@ -1022,7 +990,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 'explosion', attrs={ 'position': self._puck.node.position, - 'color': (0, 0, 0), + 'color': (0,0,0), 'radius': .1, 'big': True, } @@ -1054,7 +1022,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): for player in team.players: if player.actor: - player.actor.handlemessage(bs.CelebrateMessage(2.0)) # yay we scored + player.actor.handlemessage(bs.CelebrateMessage(2.0)) # yay we scored player.actor.percentage = min(player.actor.percentage + 15, 100) # dont mind this ;( scorch = bs.newnode( @@ -1079,8 +1047,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self._puck.scored = True # Kill the puck (it'll respawn itself shortly). - # Animation ! 0 sec = 0.25 of size in our case - bs.animate(self._puck.node, 'mesh_scale', {0: 0.25, 0.4: 0}) + bs.animate(self._puck.node, 'mesh_scale', {0: 0.25, 0.4 : 0}) # Animation ! 0 sec = 0.25 of size in our case bs.timer(0.7, self._kill_puck) bs.cameraflash(duration=7.0) @@ -1104,7 +1071,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # overriding the default character spawning.. def spawn_player(self, player: Player) -> bs.Actor: spaz = self.spawn_player_spaz(player) - spaz.bomb_count = 0 # No bombs for yall bahaha + spaz.bomb_count = 0 # No bombs for yall bahaha # Now we want to make that your punches are useless too # But we will need the button to work so let's put the damage to 0 and some custom stuff spaz._punch_power_scale = 0 @@ -1118,9 +1085,10 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): spaz.can_curve = False + spaz.last_angle = 0 spaz.total_rotation = 0.0 - spaz.rotation_direction = 0 # 0: neutre, 1: clockwise, -1: not clockwise + spaz.rotation_direction = 0 spaz.gliding = False spaz.smashed = False @@ -1138,7 +1106,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): ) spaz.node.connectattr('position', spaz.bar, 'position') spaz.bar.always_show_health_bar = True - spaz.node.name = '' # bahaha but you'll sspazuseful + spaz.node.name = '' # bahaha but you'll sspazuseful def check(): if spaz.node.exists(): @@ -1159,6 +1127,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): else: spaz.node.name = '' + spaz.bar.hurt = min(1.0 - float(spaz.percentage) / 100, 1) # Curved ball part @@ -1174,42 +1143,12 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): dx /= length dz /= length - spaz.angle = math.degrees(math.atan2(-dx, -dz)) # trust + spaz.angle = math.degrees(math.atan2(-dx, -dz)) # trust - # checks if u turn ! Not in the game yet ! - def check_270_rotation(): - now = bs.time() - current_angle = spaz.angle - delta = current_angle - spaz.last_angle - if delta > 180: - delta -= 360 - if delta < -180: - delta += 360 - current_dir = 1 if delta > 0 else -1 - - if spaz.rotation_direction != 0 and current_dir != spaz.rotation_direction: - spaz.total_rotation = 0 - - spaz.rotation_direction = current_dir - - spaz.total_rotation += abs(delta) - - if spaz.total_rotation >= 250: - spaz.curved = now + 0.40 - spaz.total_rotation = 0 - - spaz.last_angle = current_angle spaz.check_timer1 = bs.Timer(1/30, check, repeat=True) - # spaz.check_timer2 = bs.Timer(1/30, check_270_rotation, repeat=True) - - # spaz.node.hold_node = spaz.node ... nevermind - - # By the way when you have a node but want his player just do - # player = node.getdelegate(object) - # i dont know why you'll need it but here it is return spaz @@ -1225,17 +1164,17 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # Respawn dead pucks. elif isinstance(msg, PuckDiedMessage): if not self.has_ended(): - bs.timer(2, self._spawn_puck) # 2 secs to breathe bahahaha + bs.timer(2, self._spawn_puck) else: super().handlemessage(msg) + def _spawn_puck(self) -> None: self._swipsound.play() self._whistle_sound.play() assert self._puck_spawn_pos is not None self._puck = Puck(position=self._puck_spawn_pos) - self.timer = bs.Timer(1 / 144, self.lock_it, repeat=True) # 144 ?.. yes - + self.timer = bs.Timer(1 / 144, self.lock_it, repeat=True) def lock_it(self): if self._puck is None: return @@ -1290,4 +1229,5 @@ class ModInstaller: bui.screenmessage("Installation Failed!", color=(1, 0, 0)) bui.getsound('kronk2').play() + bs.apptimer(2.5, _do_install) From 7c718cf4a345bfc05deda459756779025942040f Mon Sep 17 00:00:00 2001 From: Loup <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:21:09 +0530 Subject: [PATCH 60/84] Revert "Volley Punch" This reverts commit f6c0fd5fbe5f491fa365cc34d1a3117e562b400c. --- plugins/minigames.json | 19 - plugins/minigames/volley_punch.py | 1293 ----------------------------- 2 files changed, 1312 deletions(-) delete mode 100644 plugins/minigames/volley_punch.py diff --git a/plugins/minigames.json b/plugins/minigames.json index 9ba5818..969670f 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1663,25 +1663,6 @@ "md5sum": "33e6b93420cf6115e992aca534d76bfd" } } - }, - "volley_punch": { - "description": "Have fun with your friends in this new volley game !", - "external_url": "https://github.com/Scriptz1/Learn.py-Installer/blob/main/volleypunch.py", - "authors": [ - { - "name": "Learn.py", - "email": "phillipians36@gmail.com", - "discord": "Learning .py" - } - ], - "versions": { - "1.0.0": { - "api_version": 9, - "commit_sha": "576a9c0", - "released_on": "01-06-2026", - "md5sum": "be998b599896ed0e8dd5f2545fae6173" - } - } } } } \ No newline at end of file diff --git a/plugins/minigames/volley_punch.py b/plugins/minigames/volley_punch.py deleted file mode 100644 index c19e269..0000000 --- a/plugins/minigames/volley_punch.py +++ /dev/null @@ -1,1293 +0,0 @@ -# VolleyPunch ? ig i'll name it like that - -# Made by Learn.py -# but this was def inspired by some freaky guy - - -# ba_meta require api 9 - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import babase -import _babase -import math -import random -import bascenev1 as bs -import bauiv1 as bui -import math -import time -import babase -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 - - -plugman = dict( - plugin_name="volley_punch", - description="Have fun with your friends in this new volley game !", - external_url="https://github.com/Scriptz1/Learn.py-Installer/blob/main/volleypunch.py", - authors=[ - {"name": "Learn.py", "email": "phillipians36@gmail.com", "discord": "Learning .py"}, - ], - version="1.0.0", -) - - -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 - - self.touches = 0 - assert activity is not None - assert isinstance(activity, VolleyBallGame) - pmats = [shared.object_material, activity.puck_material] - - # so this is the volleyball we gonna mesh with today - 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.3, - 'mesh_scale': activity.ball_size, - 'body_scale': 1.07, - 'gravity_scale': 1, - 'is_area_of_interest': True, - 'position': self._spawn_pos, - 'materials': pmats - }) - - 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. - # copied that stuff from hockey game ngl - 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 = 'VolleyPunch' - description = 'Punch the Volleyball !' - 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', 4.0), - ], - default=1.0, - ), - bs.FloatSetting( - 'Ball size (m)', - min_value=0.15, - default=0.25, - max_value=0.35, - increment=0.05, - ), - bs.FloatChoiceSetting( - 'Ball Average Speed', - choices=[ - ('Slower', 0.9899), - ('Normal', 1.0), - ], - default=1.0, - ), - bs.BoolSetting('Epic Mode', False), - bs.BoolSetting('Enable 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 ['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.one_sound = bs.getsound('announceOne') - self.two_sound = bs.getsound('announceTwo') - self.three_sound = bs.getsound('announceThree') # unused.. yet - self._swipsound = bs.getsound('swip') - self._whistle_sound = bs.getsound('refWhistle') - self.puck_mesh = bs.getmesh('shield') - self.puck_tex = bs.gettexture('ouyaUButton') - self._puck_sound = bs.getsound('metalHit') - self.puck_material = bs.Material() - self.puck_material.add_actions(actions=(('modify_part_collision', - 'friction', 4))) - 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), - ('modify_part_collision', 'physical', False))) - - # 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._fake_wall_material = bs.Material() # i'll fix this. trust - - self._net_wall_material = bs.Material() - self._net_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_material.add_actions( - conditions=('they_have_material', shared.object_material), - actions=( - ('modify_part_collision', 'collide', True), - ('modify_part_collision', 'physical', True), - ('modify_part_collision', - 'friction', -10), - ('call', 'at_connect', self.slow_down))) - - 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.credit_text = bool(settings['Enable Credits']) # i did the freaku credit thing - self._epic_mode = bool(settings['Epic Mode']) - - # the ball is too fast for the players and the map is soo huge its a good poc - brostos - self.ball_size = float(settings['Ball size (m)']) - self.ball_speed = float(settings['Ball Average Speed']) - - # 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.s: list = [] - self.w: list = [] - self.n: list = [] - self.last_interaction_time = -9999 - self.smashed = -9999 - self.interactions_timer = None - self.curve = True - self.difficulty = 0.5 - self.smasher = None - self.commentary_text = None - self._text_task = None # Référence pour annuler l'animation en cours - self._clear_timer = None # Référence pour le timer de disparition - self.touches_indicator = None - self.guide = None - self.curving = False - - 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._puck_spawn_pos = (-4, 1, 0) if random.random() < 0.5 else (4, 1, 0) - self._spawn_puck() - - # Set up the two score regions. - self._score_regions = [] - self.commentary_text = bs.newnode( - 'text', - attrs=dict( - text='', - scale=0.012, - position=(0, 3.1, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1, 1, 1, 0.5), - h_align='center', - v_align='top', - h_attach='center', - v_attach='top', - )) - self.touches_indicator = bs.newnode( - 'text', - attrs=dict( - text='', - scale=0.025, - position=(0, 3, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1, 1, 1, 0.5), - h_align='center', - v_align='top', - h_attach='center', - v_attach='top', - )) - self.guide = bs.newnode( - 'text', - attrs=dict( - text='PUNCH TO TAP THE BALL \nPICKUP TO CUFF \nBOMB TO GLIDE / DIVE \nJUMP UNDER A MID AIR BALL WITH THE BAR FILLED TO SMASH \nTHE ULT BAR FILLS AS YOU PLAY', - scale=0.005, - position=(0, 2.7, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1, 1, 1, 0.5), - h_align='center', - v_align='top', - h_attach='center', - v_attach='top', - )) - bs.animate_array(self.guide, 'color', 4, {0: (1, 1, 1, 0.5), 1: ( - 1, 1, 1, 1), 2: (1, 1, 1, 0.5)}, loop=True) - self.interactions_timer = bs.Timer( - 1 / 120, self.interactions, repeat=True) # time, function, repeat - self._score_regions.append( - bs.NodeActor( - bs.newnode('region', - attrs={ - 'position': (9, 0, 0), - 'scale': (17.3, 0.002, 12), - 'type': 'box', - 'materials': [self._score_region_material] - }))) - self._score_regions.append( - bs.NodeActor( - bs.newnode('region', - attrs={ - 'position': (-9, 0, 0), - 'scale': (17.3, 0.002, 20), - '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 Learn.py", - 'scale': 0.7, - 'position': (0, 0), - 'shadow': 0.5, - 'flatness': 1.2, - 'color': (1, 1, 1), - 'h_align': 'center', - 'v_attach': 'bottom'}) - - # Create the net and regions. - mat = bs.Material() # spam this whenever u want a material THEN add some properties - mat.add_actions(actions=('modify_part_collision', - 'collide', False)) - net = bs.newnode( - 'prop', - attrs={ - 'body': 'puck', - 'body_scale': 0, - 'position': (0, 0, 0), - 'mesh_scale': 1, - 'mesh': bs.getmesh('volleyball_net'), - 'color_texture': bs.gettexture('black'), - 'reflection': 'soft', - 'reflection_scale': [1.5], - 'sticky': False, - 'shadow_size': 0.2, - 'gravity_scale': 0, - 'materials': [mat], - }, - ) - - self.s.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.w.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( - 0.6, 10, 20), 'type': 'box', 'materials': (self._net_wall_material, )}))) - self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( - 0.6, 2.75, 20), 'type': 'box', 'materials': (self._net_material, )}))) - self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, -5.9), 'scale': ( - 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) - self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 5.9), 'scale': ( - 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) - - def slow_down(self): - if not self._puck: - return - puck_node = self._puck.node - mult = 0.88 - v = puck_node.velocity - puck_node.velocity = (v[0] * mult, v[1] * mult, v[2] * mult) - - def write(self, text, color): - """types""" - if hasattr(self, '_timers'): - for t in self._timers: - t = None - self._timers = [] - - if not text: - return - - self.commentary_text.color = color - total_chars = len(text) - - interval = 0.9 / max(total_chars, 1) - - for i in range(1, total_chars + 1): - def show(index=i): - if self.commentary_text.exists(): - self.commentary_text.text = text[:index] - - self._timers.append(bs.Timer(interval * i, show, repeat=False)) - - def start_clear(): - for i in range(total_chars, -1, -1): - def hide(index=i): - if self.commentary_text.exists(): - self.commentary_text.text = text[:index] - - self._timers.append( - bs.Timer(0.9 + 3.0 + (0.015 * (total_chars - i)), hide, repeat=False)) - - start_clear() - - def vignette(self, final_color, longer): - """Aesthetic reasons.""" - glb = bs.getactivity().globalsnode - base_color = (0.57, 0.57, 0.57) - bs.animate_array(glb, 'vignette_outer', 3, {0: base_color, 0.1: ( - final_color[0] / 1.2, final_color[1] / 1.2, final_color[2] / 1.2), (1 if longer else 0.5): base_color}) - - def interactions(self): - """Some personalized stuff and my deepest secrets. - I don't know why, but I feel SoK may like this.""" - if self._puck is None: - return - - vel = self._puck.node.velocity - # apply the mult - self._puck.node.velocity = ( - vel[0], - vel[1] * self.ball_speed, - vel[2] - ) - - green = bs.gettexture('ouyaOButton') - yellow = bs.gettexture('ouyaYButton') - red = bs.gettexture('ouyaAButton') - blue = bs.gettexture('ouyaUButton') - - # If it passes the middle.. green. - puck_node = self._puck.node - pos = puck_node.position - x_diff = abs(pos[0]) - - if x_diff < 0.25 or self.curve: - puck_node.color_texture = blue - self._puck.touches = 0 - - if pos[1] < 2.6 and (bs.time() - self.last_interaction_time > 0.5): - mult = 0.935 - v = puck_node.velocity - puck_node.velocity = (v[0] * mult, v[1] * mult, v[2] * mult) - - self.save_text = random.choice([ - "dived to the rescue!", - "kept the ball alive!", - "flew in to save the day!", - "is too fast for the floor!", - "bounced it back just in time!", - "kept the dream alive!", - "barely missed the ground!", - "saved the rally!", - "made a clutch save!", - "refused to let it drop!", - "pulled a superman save!", - "made a desperate save!", - "proved the floor is not an option!", - "landed a miracle touch!", - "saved the point!", - "pulled off a heroic dive!", - "stretched to the limit!", - "barely kept it up!", - "showed reflexes of a cat!", - "denied the floor!", - "made a clutch save!", - "is staying in the game!", - "pulled an epic recovery!", - "reacted in the nick of time!", - "won't let the ball drop!", - "is pure hustle!", - "made an impossible save!", - "is defying gravity!", - "made sure the ground stays empty!", - "saved it!", - "pulled off last-second magic!", - "is floating to save it!", - "is too clutch to fail!", - "got the ball back in the air!", - "is denying the dirt!", - "saved it by a hair!", - "is fighting for every ball!", - "is pure energy!", - "never gives up!", - "keeps it in play!", - "saved it barely!", - "kept the floor clean!", - "is pure determination!", - "pulled a miraculous save!", - "denied the game over!", - "is still alive!", - "is keeping the rally going!", - "made an out-of-reach save!", - "made a ninja-like save!", - "bounced back from trouble!"]) - self.hit_text = random.choice([ - "sent it over!", - "cleared the net!", - "powered it across!", - "nailed a deep shot!", - "bounced it into their zone!", - "launched an attack!", - "sent a rocket over!", - "is dominating the net!", - "found the open spot!", - "placed it perfectly!", - "smashed it back!", - "is applying the pressure!", - "sent them scrambling!", - "over the net it goes!", - "hit a beauty!", - "is controlling the court!", - "forced a tough return!", - "keeps the pressure high!", - "landed a sweet strike!", - "sent it deep!", - "is playing aggressively!", - "hit a laser beam!", - "made a solid connection!", - "is taking charge!", - "sent a tricky shot over!", - "is firing back!", - "put it right on target!", - "sent the ball flying!", - "beat the defense!", - "is crushing it!", - "nailed that serve return!", - "showed off some power!", - "sent a high arc over!", - "is making moves!", - "kept them on their toes!", - "delivered a powerful hit!", - "is pushing the pace!", - "found the gap!", - "sent a curveball over!", - "is looking sharp!", - "hit it with precision!", - "made them work for it!", - "is owning the net!", - "sent a heavy strike!", - "is dictating the play!", - "put it out of reach!", - "landed the perfect hit!", - "is on fire!", - "sent a swift strike!", - "is forcing errors!" - ]) - self.control_text = random.choice([ - "is showing off sweet control!", - "has the ball on a string!", - "handles it with grace!", - "shows off some nice touch!", - "keeps it close and tight!", - "is demonstrating perfect poise!", - "has ice in their veins!", - "is keeping it under control!", - "shows a velvet touch!", - "is masterfully handling the ball!", - "keeps the ball glued to them!", - "is displaying total composure!", - "makes it look so easy!", - "is controlling the pace!", - "shows off pure finesse!", - "has the ball perfectly placed!", - "is staying cool under pressure!", - "has incredible ball handling!", - "is maneuvering with ease!", - "keeps the ball within reach!", - "shows off some fancy footwork!", - "is keeping the ball calm!", - "has the softest touch!", - "is dictating the rhythm!", - "keeps it balanced and steady!", - "is showing high-level control!", - "has the situation handled!", - "is playing with surgical precision!", - "keeps the ball in their zone!", - "shows off expert handling!", - "is keeping it smooth!", - "has the ball right where they want it!", - "demonstrates a gentle touch!", - "is keeping the game slow!", - "shows off calm concentration!", - "has everything under control!", - "is moving with grace!", - "keeps the ball softly hovering!", - "shows off true talent!", - "is maintaining perfect flow!", - "has a magical touch!", - "is playing it smart!", - "keeps the ball very close!", - "shows off professional control!", - "is handling the pressure well!", - "has a very steady hand!", - "is keeping it precise!", - "shows off masterful technique!", - "is keeping the ball in check!", - "has the perfect feel for it!" - ]) - if self.touches_indicator is not None and self.touches_indicator.exists(): - self.touches_indicator.position = (self._puck.node.position[0], self._puck.node.position[1] + 0.35, - self._puck.node.position[2]) - else: - self.touches_indicator = bs.newnode( - 'text', - attrs=dict( - text='', - scale=0.025, - position=(0, 3, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1, 1, 1, 0.5), - h_align='center', - v_align='top', - h_attach='center', - v_attach='top', - )) - - everything = bs.getnodes() - everyone = [] - closest_distance = 9999 - closest_player = None - - # i won't explain what "curve" is... look by yourself - if not self.curve and not self._puck.scored: - if bs.time() < self.smashed: - random_pos = (random.uniform( - 0.1, 0.7) + self._puck.node.position[0], self._puck.node.position[1], random.uniform(0.1, 0.7) + self._puck.node.position[2]) - e = bs.newnode( - 'explosion', - attrs={ - 'position': random_pos, - 'color': self.smasher.color, - 'radius': random.uniform(0.1, 0.7), - 'big': False, - } - ) - bs.timer(1, e.delete) - self.smasher.getdelegate(object).percentage /= 1.1 - else: - ball_tex = self._puck.node.color_texture - - texture_to_name = { - green: 'green', - yellow: 'yellow', - red: 'red', - blue: 'blue' - } - - colors = { - 'green': (0.6, 1, 0.6), - 'yellow': (1, 0.3, 0), - 'red': (1, 0, 0), - 'blue': (0.6, 0.6, 1) - } - - color_name = texture_to_name.get(ball_tex) - color = colors.get(color_name) - - e = bs.newnode( - 'explosion', - attrs={ - 'position': self._puck.node.position, - 'color': color, - 'radius': 0.1, - 'big': False, - } - ) - indicator_color = (*color, 0.5) - bs.timer(1, e.delete) - self.touches_indicator.color = indicator_color - self.touches_indicator.text = str(3 - self._puck.touches) - - for human in everything: # doesn't make much sense, but I hope u understand I want players nodes. - if human.getnodetype() == "spaz": - everyone.append(human) - - for player_node in everyone: - if not player_node.exists(): # why wouldn't u exist at first - continue - - # not wanting to do that Euclidean thingy - distance = math.dist(player_node.position, self._puck.node.position) - player_node.hold_node = None - player_node.getdelegate(object).impact_scale = 0 - if distance < closest_distance: - closest_player = player_node # him, the closest one - closest_distance = distance # his distance with the volleyball - - good: bool = (self._puck.touches < 3 and - bs.time() - self.last_interaction_time > 0.4 and - not self._puck.scored) - # bs.time() is the current bombsquad time :) bs for bombsquad. - - delegate = closest_player.getdelegate(object) - now = bs.time() - can_jump = (delegate.last_jump_time_ms > - delegate._jump_cooldown and closest_player.position[1] < 1) - - if closest_player.jump_pressed and can_jump: - ppos = closest_player.position - delegate.last_jump_time_ms = int(bs.time() * 1000) - closest_player.handlemessage( - 'kick_back', - ppos[0], - ppos[1], - ppos[2], - 0, - 1, - 0, - 50, # small force - ) - - punched = now < delegate.punched - pickup = now < delegate.pickup - bombed = now < delegate.bombed - curved = now < delegate.curved - - # SO if broski is the nearest AND broski is near enough... - if closest_player and closest_distance < 4 and good: - player = spaz = delegate - ball = self._puck.node # ball node - you = closest_player.position # already a node - in_air = (you[1] > 0.8) - # Ball goes where you look but always tries to go to the other side. (tell me if u guys want it to go totally where you're looking at) - if closest_distance < (1.35 if not in_air else 1.6) and (closest_player.punch_pressed or punched): - self.difficulty += 0.6 / 75 - e = bs.newnode( - 'explosion', - attrs={ - 'position': self._puck.node.position, - 'color': closest_player.color, - 'radius': 0.8, - 'big': False, - } - ) - bs.timer(1, e.delete) - self.last_interaction_time = bs.time() - self._puck.touches += 1 - self.curve = False - if self._puck.touches == 2: - spaz.percentage = min(spaz.percentage + 8, 100) - ball.color_texture = yellow - self.two_sound.play() - elif self._puck.touches == 3: - spaz.percentage = min(spaz.percentage + 12, 100) - ball.color_texture = red - self.one_sound.play() - else: - ball.color_texture = green - # This is for the sound, you probably didn't know that! - bs.getsound("impactHard").play() - - sight = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward) - ).normalized() * (12 if delegate.percentage < 95 else 30) # Sight factor - speed = 6 if delegate.percentage < 95 else 20 # bar full - - if curved: - start_pos = ball.position - end_pos = (sight[0] * 2, -1, -sight[2]) - duration = 1.1 - fps = 144 - total_steps = int(duration * fps) - curved_time = bs.time() + 0.15 - self.curving = True - - z_offset = -4.0 if sight[0] < 0 else 4.0 - - def move_to_point(step): - if not ball.exists(): - return - if ball.position[1] < 1 and bs.time() > curved_time: - return - if not self.curving: - return - - t = step / total_steps - - x = start_pos[0] + (end_pos[0] - start_pos[0]) * t - y = start_pos[1] + (end_pos[1] - start_pos[1]) * t - z = start_pos[2] + (end_pos[2] - start_pos[2]) * t - - y += math.sin(t * math.pi) * 3.0 - - # Ajout de la courbe latérale - z += math.sin(t * math.pi) * z_offset - - # --- LIMITE SUR L'AXE Z --- - # On force z à rester entre -5 et 5 - z = max(-5.0, min(5.0, z)) - # -------------------------- - - ball.position = (x, y, z) - - if step < total_steps: - bs.timer(1 / fps, lambda: move_to_point(step + 1)) - - move_to_point(0) - else: - # normal - self.curving = False - if in_air and delegate.percentage > 95: - if closest_player.position[0] < 0: - vel = (12 * 1.6, 0, sight[2]) * speed - else: - vel = (-12 * 1.6, 0, sight[2]) * speed - else: - if closest_player.position[0] < 0: - vel = (4.8 * 1.6, 0, sight[2]) * speed - else: - vel = (-4.8 * 1.6, 0, sight[2]) * speed - - # smth - dist_from_middle = math.dist(you, (0, 2, 0)) - height = (max(dist_from_middle * 2.75, 7) * 0.8) - if in_air and delegate.percentage > 95: - height = -4 * 2 / dist_from_middle - - ball.velocity = (vel[0], height, vel[2]) - if in_air and delegate.percentage > 95: - audio = random.choice(['explosion01', 'explosion02', - 'explosion03', 'explosion04', 'explosion05']) - bs.getsound(audio).play() - self.smashed = bs.time() + 1 - self.smasher = closest_player - self.vignette(closest_player.color, False) - delegate.on_punch_press() - delegate.on_punch_release() - self.write( - f'{player.name.upper()} SMASHED THE BALL !' if random.random() < 0.5 else f'{player.name} hit the ball with all his might !', closest_player.color) - else: - self.write( - f'{player.name} {self.hit_text}', closest_player.color) - - # prettiness here - ppos = you - f = (bs.Vec3(ball.position) - bs.Vec3(you)).normalized() - closest_player.handlemessage( - 'kick_back', - ppos[0], - ppos[1], - ppos[2], - f[0], - f[1], - f[2], - 400, # small force - ) - # Ball goes straight up in the air. idk what it is called. - if closest_distance < 1.35 and (closest_player.pickup_pressed or pickup): - self.write( - f'{player.name} {self.control_text}', closest_player.color) - y_diff = abs(self._puck.node.position[1] - closest_player.position[1]) - - if y_diff < 1: - self.difficulty += 0.6 / 75 - self.last_interaction_time = bs.time() - self.curve = False - - self._puck.touches += 1 - - ball = self._puck.node - if self._puck.touches == 2: - spaz.percentage = min(spaz.percentage + 4, 100) - ball.color_texture = yellow - self.two_sound.play() - elif self._puck.touches == 3: - spaz.percentage = min(spaz.percentage + 6, 100) - ball.color_texture = red - self.one_sound.play() - else: - ball.color_texture = green - - closest_player.handlemessage('celebrate', 400) - bs.getsound("impactMedium").play() - e = bs.newnode( - 'explosion', - attrs={ - 'position': self._puck.node.position, - 'color': closest_player.color, - 'radius': 0.5, - 'big': True, - } - ) - bs.timer(1, e.delete) - - direction = (bs.Vec3(you) - - bs.Vec3(closest_player.position_forward)).normalized() - ball.velocity = (-you[0] * 0.04, 12.8, direction[2] * 0.1) - if closest_player.bomb_pressed and not delegate.gliding or bombed and not delegate.gliding: # gliding.. variant coming soon - self.write( - f'{player.name} {self.save_text}', closest_player.color) - delegate.gliding = True - ppos = you - f = (bs.Vec3(ball.position) - bs.Vec3(you)).normalized() - distance = math.dist(ball.position, you) - closest_player.handlemessage( - 'kick_back', - ppos[0], - ppos[1], - ppos[2], - f[0], - 0, - f[2], - 1000 - (500/distance), # tiny force - ) - closest_player.handlemessage('knockout', 100) - print(1) - spaz.pickup = now + 0.8 - - bs.getsound("impactHard").play(volume=0.5) - e = bs.newnode( - 'explosion', - attrs={ - 'position': you, - 'color': (0, 0.3, 0.5), - 'radius': 1.1, - 'big': False, - } - ) - bs.timer(1, e.delete) - bs.timer(0.6, lambda: setattr(delegate, 'gliding', False)) - - # variant soon - - 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 - self.curve = True - - 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 - # to not be getting a corrupted score. - if self._puck.scored: - return - E = bs.newnode( - 'explosion', - attrs={ - 'position': self._puck.node.position, - 'color': (0, 0, 0), - 'radius': .1, - 'big': True, - } - ) - bs.timer(1, E.delete) - bs.animate(self.touches_indicator, 'scale', {0: 0, 2.65: 0, 3: 0.025}) - self.touches_indicator.text = '0' - if self.smashed > 0: - self._puck.node.sticky = True - self.smashed = -9999 - - 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: - team.score += 1 - - # Puck Spawn - if team.id == 0: # left side scored - self._puck_spawn_pos = (5, 0.7, 0) - elif team.id == 1: # right side scored - self._puck_spawn_pos = (-5, 0.7, 0) - else: # what the heck - self._puck_spawn_pos = (0, 0.7, 0) - - for player in team.players: - if player.actor: - player.actor.handlemessage(bs.CelebrateMessage(2.0)) # yay we scored - player.actor.percentage = min(player.actor.percentage + 15, 100) - # dont mind this ;( - scorch = bs.newnode( - 'scorch', - attrs={ - 'position': self._puck.node.position, - 'size': 1.4, - 'color': player.actor.node.color, - 'big': True, - }, - ) - bs.animate(scorch, 'presence', {3.000: 1, 7.000: 0}) - bs.timer(7, scorch.delete) - self.vignette(player.actor.node.color, longer=True) - - # End game if we won. - if team.score >= self._score_to_win: - self.end_game() - - self._cheer_sound.play() - - self._puck.scored = True - - # Kill the puck (it'll respawn itself shortly). - # Animation ! 0 sec = 0.25 of size in our case - bs.animate(self._puck.node, 'mesh_scale', {0: 0.25, 0.4: 0}) - 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.interactions_timer = None - self.end(results=results) - - def on_transition_in(self) -> None: - super().on_transition_in() - - 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) - spaz.bomb_count = 0 # No bombs for yall bahaha - # Now we want to make that your punches are useless too - # But we will need the button to work so let's put the damage to 0 and some custom stuff - spaz._punch_power_scale = 0 - spaz.impact_scale = 0 - spaz.check_timer = 0 - - spaz.pickup = 0 - spaz.punched = 0 - spaz.bombed = 0 - spaz.curved = 0 - - spaz.can_curve = False - - spaz.last_angle = 0 - spaz.total_rotation = 0.0 - spaz.rotation_direction = 0 # 0: neutre, 1: clockwise, -1: not clockwise - - spaz.gliding = False - spaz.smashed = False - spaz.percentage = 0 - spaz._jump_cooldown = 2000 - spaz.name = spaz.node.name - spaz.bar = bs.newnode( - 'shield', - owner=spaz.node, - attrs={ - 'position': spaz.node.position, - 'radius': .01, - 'color': (0, 0, 0), - } - ) - spaz.node.connectattr('position', spaz.bar, 'position') - spaz.bar.always_show_health_bar = True - spaz.node.name = '' # bahaha but you'll sspazuseful - - def check(): - if spaz.node.exists(): - # Controls part - now = bs.time() - - if spaz.node.punch_pressed: - spaz.punched = now + 0.25 - elif spaz.node.pickup_pressed: - spaz.pickup = now + 0.40 - elif spaz.node.bomb_pressed: - spaz.bombed = now + 0.20 - elif spaz.node.jump_pressed and spaz.percentage > 95: - spaz.punched = now + 0.50 - - if spaz.percentage > 95: - spaz.node.name = 'SMASH' - else: - spaz.node.name = '' - - spaz.bar.hurt = min(1.0 - float(spaz.percentage) / 100, 1) - - # Curved ball part - - pos = spaz.node.position - fwd = spaz.node.position_forward - - dx = fwd[0] - pos[0] - dz = fwd[2] - pos[2] - - length = math.sqrt(dx * dx + dz * dz) - if length > 0.0001: - dx /= length - dz /= length - - spaz.angle = math.degrees(math.atan2(-dx, -dz)) # trust - - # checks if u turn ! Not in the game yet ! - def check_270_rotation(): - now = bs.time() - current_angle = spaz.angle - - delta = current_angle - spaz.last_angle - if delta > 180: - delta -= 360 - if delta < -180: - delta += 360 - - current_dir = 1 if delta > 0 else -1 - - if spaz.rotation_direction != 0 and current_dir != spaz.rotation_direction: - spaz.total_rotation = 0 - - spaz.rotation_direction = current_dir - - spaz.total_rotation += abs(delta) - - if spaz.total_rotation >= 250: - spaz.curved = now + 0.40 - spaz.total_rotation = 0 - - spaz.last_angle = current_angle - - spaz.check_timer1 = bs.Timer(1/30, check, repeat=True) - # spaz.check_timer2 = bs.Timer(1/30, check_270_rotation, repeat=True) - - # spaz.node.hold_node = spaz.node ... nevermind - - # By the way when you have a node but want his player just do - # player = node.getdelegate(object) - # i dont know why you'll need it but here it is - - return spaz - - def handlemessage(self, msg: Any) -> Any: - - # Respawn dead players if they're still in the game. - # Because if we don't.. they won't like our 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, self._spawn_puck) # 2 secs to breathe bahahaha - else: - super().handlemessage(msg) - - def _spawn_puck(self) -> None: - self._swipsound.play() - self._whistle_sound.play() - assert self._puck_spawn_pos is not None - self._puck = Puck(position=self._puck_spawn_pos) - self.timer = bs.Timer(1 / 144, self.lock_it, repeat=True) # 144 ?.. yes - - def lock_it(self): - if self._puck is None: - return - if self.curve: - self._puck.node.position = self._puck_spawn_pos - else: - self._puck.node.shadow_size = min(self._puck.node.position[1] * 0.3, 2) - - -# ba_meta export babase.Plugin -class justlearn(babase.Plugin): - def __init__(self): - self.installer = ModInstaller() - self.installer.run_full_install() - - -class ModInstaller: - def __init__(self) -> None: - self.python_user = _babase.env()["python_directory_user"] - self.files = self.python_user + '/VolleyBallMap/' - self.app_dir = _babase.env()["python_directory_app"] + '/' - self.data_dir = self.app_dir + '../' - self.meshes_dir = self.data_dir + 'meshes/' - self.platform = _babase.app.classic.platform - - def run_full_install(self) -> None: - import os - import urllib.request - import shutil - import babase - - target_path = os.path.join(self.files, 'volleyball_net.bob') - destination_path = os.path.join(self.meshes_dir, 'volleyball_net.bob') - - if os.path.exists(destination_path): - return - - def _do_install(): - try: - if not os.path.exists(self.files): - os.makedirs(self.files) - - url = "https://raw.githubusercontent.com/Scriptz1/Learn.py-Installer/main/volleyball_net.bob" - urllib.request.urlretrieve(url, target_path) - shutil.copy(target_path, destination_path) - - bui.screenmessage("Installation worked !", color=(0, 1, 0.3)) - bui.getsound('ding').play() - - except Exception as e: - print(f"DEBUG ERROR: {e}") - bui.screenmessage("Installation Failed!", color=(1, 0, 0)) - bui.getsound('kronk2').play() - - bs.apptimer(2.5, _do_install) From 811e151c812985111ceeb3736cfef91ec1f4213d Mon Sep 17 00:00:00 2001 From: Scriptz1 <141498178+Scriptz1@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:25:17 -0400 Subject: [PATCH 61/84] Update volley_punch.py --- plugins/minigames/volley_punch.py | 334 ++++++++++++++++++------------ 1 file changed, 197 insertions(+), 137 deletions(-) diff --git a/plugins/minigames/volley_punch.py b/plugins/minigames/volley_punch.py index 8c8b0c8..c19e269 100644 --- a/plugins/minigames/volley_punch.py +++ b/plugins/minigames/volley_punch.py @@ -10,8 +10,10 @@ from __future__ import annotations from typing import TYPE_CHECKING -import babase, _babase -import math, random +import babase +import _babase +import math +import random import bascenev1 as bs import bauiv1 as bui import math @@ -28,7 +30,7 @@ if TYPE_CHECKING: plugman = dict( - plugin_name="VolleyPunch", + plugin_name="volley_punch", description="Have fun with your friends in this new volley game !", external_url="https://github.com/Scriptz1/Learn.py-Installer/blob/main/volleypunch.py", authors=[ @@ -37,6 +39,7 @@ plugman = dict( version="1.0.0", ) + class PuckDiedMessage: """Inform something that a puck has died.""" @@ -78,9 +81,6 @@ class Puck(bs.Actor): 'materials': pmats }) - - - def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.DieMessage): assert self.node @@ -197,7 +197,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self._chant_sound = bs.getsound('crowdChant') self.one_sound = bs.getsound('announceOne') self.two_sound = bs.getsound('announceTwo') - self.three_sound = bs.getsound('announceThree') # unused.. yet + self.three_sound = bs.getsound('announceThree') # unused.. yet self._swipsound = bs.getsound('swip') self._whistle_sound = bs.getsound('refWhistle') self.puck_mesh = bs.getmesh('shield') @@ -243,7 +243,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', self._handle_score))) - self._fake_wall_material = bs.Material() #i'll fix this. trust + self._fake_wall_material = bs.Material() # i'll fix this. trust self._net_wall_material = bs.Material() self._net_material = bs.Material() @@ -263,30 +263,25 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 'friction', -10), ('call', 'at_connect', self.slow_down))) - - - 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.credit_text = bool(settings['Enable Credits']) # i did the freaku credit thing + self.credit_text = bool(settings['Enable Credits']) # i did the freaku credit thing self._epic_mode = bool(settings['Epic Mode']) - # the ball is too fast for the players and the map is soo huge its a good poc - brostos self.ball_size = float(settings['Ball size (m)']) self.ball_speed = float(settings['Ball Average Speed']) - # 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.s : list = [] - self.w : list = [] - self.n : list = [] + self.s: list = [] + self.w: list = [] + self.n: list = [] self.last_interaction_time = -9999 self.smashed = -9999 self.interactions_timer = None @@ -314,58 +309,60 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): super().on_begin() self.setup_standard_time_limit(self._time_limit) - self._puck_spawn_pos = (-4, 1 ,0) if random.random() < 0.5 else (4, 1, 0) + self._puck_spawn_pos = (-4, 1, 0) if random.random() < 0.5 else (4, 1, 0) self._spawn_puck() # Set up the two score regions. self._score_regions = [] self.commentary_text = bs.newnode( - 'text', - attrs=dict( - text='', - scale=0.012, - position=(0, 3.1, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1,1,1,0.5), - h_align= 'center', - v_align= 'top', - h_attach= 'center', - v_attach= 'top', - )) + 'text', + attrs=dict( + text='', + scale=0.012, + position=(0, 3.1, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) self.touches_indicator = bs.newnode( - 'text', - attrs=dict( - text='', - scale=0.025, - position=(0, 3, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1,1,1,0.5), - h_align= 'center', - v_align= 'top', - h_attach= 'center', - v_attach= 'top', - )) + 'text', + attrs=dict( + text='', + scale=0.025, + position=(0, 3, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) self.guide = bs.newnode( - 'text', - attrs=dict( - text='PUNCH TO TAP THE BALL \nPICKUP TO CUFF \nBOMB TO GLIDE / DIVE \nJUMP UNDER A MID AIR BALL WITH THE BAR FILLED TO SMASH \nTHE ULT BAR FILLS AS YOU PLAY', - scale=0.005, - position=(0, 2.7, -5.8), - in_world=True, - flatness=1, - shadow=0, - color=(1,1,1,0.5), - h_align= 'center', - v_align= 'top', - h_attach= 'center', - v_attach= 'top', - )) - bs.animate_array(self.guide, 'color',4, {0: (1,1,1,0.5), 1: (1,1,1,1), 2: (1,1,1,0.5)}, loop=True) - self.interactions_timer = bs.Timer(1 / 120, self.interactions, repeat=True) # time, function, repeat + 'text', + attrs=dict( + text='PUNCH TO TAP THE BALL \nPICKUP TO CUFF \nBOMB TO GLIDE / DIVE \nJUMP UNDER A MID AIR BALL WITH THE BAR FILLED TO SMASH \nTHE ULT BAR FILLS AS YOU PLAY', + scale=0.005, + position=(0, 2.7, -5.8), + in_world=True, + flatness=1, + shadow=0, + color=(1, 1, 1, 0.5), + h_align='center', + v_align='top', + h_attach='center', + v_attach='top', + )) + bs.animate_array(self.guide, 'color', 4, {0: (1, 1, 1, 0.5), 1: ( + 1, 1, 1, 1), 2: (1, 1, 1, 0.5)}, loop=True) + self.interactions_timer = bs.Timer( + 1 / 120, self.interactions, repeat=True) # time, function, repeat self._score_regions.append( bs.NodeActor( bs.newnode('region', @@ -397,16 +394,16 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 'h_align': 'center', 'v_attach': 'bottom'}) - #Create the net and regions. - mat = bs.Material() # spam this whenever u want a material THEN add some properties + # Create the net and regions. + mat = bs.Material() # spam this whenever u want a material THEN add some properties mat.add_actions(actions=('modify_part_collision', - 'collide', False)) + 'collide', False)) net = bs.newnode( 'prop', attrs={ 'body': 'puck', 'body_scale': 0, - 'position': (0,0,0), + 'position': (0, 0, 0), 'mesh_scale': 1, 'mesh': bs.getmesh('volleyball_net'), 'color_texture': bs.gettexture('black'), @@ -419,7 +416,6 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): }, ) - self.s.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 2.4, 0), 'scale': ( 0.8, 6, 20), 'type': 'box', 'materials': (self._fake_wall_material, )}))) @@ -428,9 +424,9 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 0), 'scale': ( 0.6, 2.75, 20), 'type': 'box', 'materials': (self._net_material, )}))) self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, -5.9), 'scale': ( - 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material )}))) + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) self.n.append(bs.NodeActor(bs.newnode('region', attrs={'position': (0, 0, 5.9), 'scale': ( - 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material )}))) + 20, 20, 0.8), 'type': 'box', 'materials': (self._net_wall_material, self._net_material)}))) def slow_down(self): if not self._puck: @@ -468,15 +464,17 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): if self.commentary_text.exists(): self.commentary_text.text = text[:index] - self._timers.append(bs.Timer(0.9 + 3.0 + (0.015 * (total_chars - i)), hide, repeat=False)) + self._timers.append( + bs.Timer(0.9 + 3.0 + (0.015 * (total_chars - i)), hide, repeat=False)) start_clear() def vignette(self, final_color, longer): """Aesthetic reasons.""" glb = bs.getactivity().globalsnode - base_color = (0.57,0.57,0.57) - bs.animate_array(glb, 'vignette_outer', 3, {0: base_color, 0.1: (final_color[0] / 1.2, final_color[1] / 1.2, final_color[2] / 1.2), (1 if longer else 0.5): base_color}) + base_color = (0.57, 0.57, 0.57) + bs.animate_array(glb, 'vignette_outer', 3, {0: base_color, 0.1: ( + final_color[0] / 1.2, final_color[1] / 1.2, final_color[2] / 1.2), (1 if longer else 0.5): base_color}) def interactions(self): """Some personalized stuff and my deepest secrets. @@ -686,8 +684,6 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): v_attach='top', )) - - everything = bs.getnodes() everyone = [] closest_distance = 9999 @@ -696,7 +692,8 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # i won't explain what "curve" is... look by yourself if not self.curve and not self._puck.scored: if bs.time() < self.smashed: - random_pos = (random.uniform(0.1, 0.7) + self._puck.node.position[0],self._puck.node.position[1],random.uniform(0.1, 0.7) + self._puck.node.position[2]) + random_pos = (random.uniform( + 0.1, 0.7) + self._puck.node.position[0], self._puck.node.position[1], random.uniform(0.1, 0.7) + self._puck.node.position[2]) e = bs.newnode( 'explosion', attrs={ @@ -742,31 +739,31 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.touches_indicator.color = indicator_color self.touches_indicator.text = str(3 - self._puck.touches) - for human in everything: # doesn't make much sense, but I hope u understand I want players nodes. + for human in everything: # doesn't make much sense, but I hope u understand I want players nodes. if human.getnodetype() == "spaz": everyone.append(human) for player_node in everyone: - if not player_node.exists(): # why wouldn't u exist at first + if not player_node.exists(): # why wouldn't u exist at first continue - - distance = math.dist(player_node.position, self._puck.node.position) # not wanting to do that Euclidean thingy + # not wanting to do that Euclidean thingy + distance = math.dist(player_node.position, self._puck.node.position) player_node.hold_node = None player_node.getdelegate(object).impact_scale = 0 if distance < closest_distance: - closest_player = player_node # him, the closest one - closest_distance = distance # his distance with the volleyball + closest_player = player_node # him, the closest one + closest_distance = distance # his distance with the volleyball - good : bool = (self._puck.touches < 3 and - bs.time() - self.last_interaction_time > 0.4 and - not self._puck.scored) + good: bool = (self._puck.touches < 3 and + bs.time() - self.last_interaction_time > 0.4 and + not self._puck.scored) # bs.time() is the current bombsquad time :) bs for bombsquad. - delegate = closest_player.getdelegate(object) now = bs.time() - can_jump = (delegate.last_jump_time_ms > delegate._jump_cooldown and closest_player.position[1] < 1) + can_jump = (delegate.last_jump_time_ms > + delegate._jump_cooldown and closest_player.position[1] < 1) if closest_player.jump_pressed and can_jump: ppos = closest_player.position @@ -782,19 +779,19 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 50, # small force ) - punched = now < delegate.punched pickup = now < delegate.pickup bombed = now < delegate.bombed curved = now < delegate.curved - - if closest_player and closest_distance < 4 and good: # SO if broski is the nearest AND broski is near enough... + # SO if broski is the nearest AND broski is near enough... + if closest_player and closest_distance < 4 and good: player = spaz = delegate ball = self._puck.node # ball node you = closest_player.position # already a node in_air = (you[1] > 0.8) - if closest_distance < (1.35 if not in_air else 1.6) and (closest_player.punch_pressed or punched): # Ball goes where you look but always tries to go to the other side. (tell me if u guys want it to go totally where you're looking at) + # Ball goes where you look but always tries to go to the other side. (tell me if u guys want it to go totally where you're looking at) + if closest_distance < (1.35 if not in_air else 1.6) and (closest_player.punch_pressed or punched): self.difficulty += 0.6 / 75 e = bs.newnode( 'explosion', @@ -819,38 +816,78 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.one_sound.play() else: ball.color_texture = green - bs.getsound("impactHard").play() # This is for the sound, you probably didn't know that! + # This is for the sound, you probably didn't know that! + bs.getsound("impactHard").play() + sight = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward) + ).normalized() * (12 if delegate.percentage < 95 else 30) # Sight factor + speed = 6 if delegate.percentage < 95 else 20 # bar full + if curved: + start_pos = ball.position + end_pos = (sight[0] * 2, -1, -sight[2]) + duration = 1.1 + fps = 144 + total_steps = int(duration * fps) + curved_time = bs.time() + 0.15 + self.curving = True - sight = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward)).normalized() * (12 if delegate.percentage < 95 else 30) # Sight factor - speed = 6 if delegate.percentage < 95 else 20 # bar full + z_offset = -4.0 if sight[0] < 0 else 4.0 - # normal - self.curving = False - if in_air and delegate.percentage > 95: - if closest_player.position[0] < 0: - vel = (12 * 1.6, 0, sight[2]) * speed - else: - vel = (-12 * 1.6, 0, sight[2]) * speed + def move_to_point(step): + if not ball.exists(): + return + if ball.position[1] < 1 and bs.time() > curved_time: + return + if not self.curving: + return + + t = step / total_steps + + x = start_pos[0] + (end_pos[0] - start_pos[0]) * t + y = start_pos[1] + (end_pos[1] - start_pos[1]) * t + z = start_pos[2] + (end_pos[2] - start_pos[2]) * t + + y += math.sin(t * math.pi) * 3.0 + + # Ajout de la courbe latérale + z += math.sin(t * math.pi) * z_offset + + # --- LIMITE SUR L'AXE Z --- + # On force z à rester entre -5 et 5 + z = max(-5.0, min(5.0, z)) + # -------------------------- + + ball.position = (x, y, z) + + if step < total_steps: + bs.timer(1 / fps, lambda: move_to_point(step + 1)) + + move_to_point(0) else: - if closest_player.position[0] < 0: - vel = (4.8 * 1.6, 0, sight[2]) * speed + # normal + self.curving = False + if in_air and delegate.percentage > 95: + if closest_player.position[0] < 0: + vel = (12 * 1.6, 0, sight[2]) * speed + else: + vel = (-12 * 1.6, 0, sight[2]) * speed else: - vel = (-4.8 * 1.6, 0, sight[2]) * speed + if closest_player.position[0] < 0: + vel = (4.8 * 1.6, 0, sight[2]) * speed + else: + vel = (-4.8 * 1.6, 0, sight[2]) * speed - # smth - dist_from_middle = math.dist(you, (0, 2, 0)) - height = (max(dist_from_middle * 2.75, 7) * 0.8) - if in_air and delegate.percentage <= 95: - height = 3.5 - vel = (vel[0] * 1.5, height, vel[2]) - if in_air and delegate.percentage > 95: - height = -4 * 2 / dist_from_middle + # smth + dist_from_middle = math.dist(you, (0, 2, 0)) + height = (max(dist_from_middle * 2.75, 7) * 0.8) + if in_air and delegate.percentage > 95: + height = -4 * 2 / dist_from_middle - ball.velocity = (vel[0], height, vel[2]) + ball.velocity = (vel[0], height, vel[2]) if in_air and delegate.percentage > 95: - audio = random.choice(['explosion01','explosion02','explosion03','explosion04','explosion05']) + audio = random.choice(['explosion01', 'explosion02', + 'explosion03', 'explosion04', 'explosion05']) bs.getsound(audio).play() self.smashed = bs.time() + 1 self.smasher = closest_player @@ -863,7 +900,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.write( f'{player.name} {self.hit_text}', closest_player.color) - #prettiness here + # prettiness here ppos = you f = (bs.Vec3(ball.position) - bs.Vec3(you)).normalized() closest_player.handlemessage( @@ -876,7 +913,8 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): f[2], 400, # small force ) - if closest_distance < 1.35 and (closest_player.pickup_pressed or pickup): # Ball goes straight up in the air. idk what it is called. + # Ball goes straight up in the air. idk what it is called. + if closest_distance < 1.35 and (closest_player.pickup_pressed or pickup): self.write( f'{player.name} {self.control_text}', closest_player.color) y_diff = abs(self._puck.node.position[1] - closest_player.position[1]) @@ -886,7 +924,6 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self.last_interaction_time = bs.time() self.curve = False - self._puck.touches += 1 ball = self._puck.node @@ -914,9 +951,10 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): ) bs.timer(1, e.delete) - direction = (bs.Vec3(you) - bs.Vec3(closest_player.position_forward)).normalized() + direction = (bs.Vec3(you) - + bs.Vec3(closest_player.position_forward)).normalized() ball.velocity = (-you[0] * 0.04, 12.8, direction[2] * 0.1) - if closest_player.bomb_pressed and not delegate.gliding or bombed and not delegate.gliding: # gliding.. variant coming soon + if closest_player.bomb_pressed and not delegate.gliding or bombed and not delegate.gliding: # gliding.. variant coming soon self.write( f'{player.name} {self.save_text}', closest_player.color) delegate.gliding = True @@ -952,12 +990,6 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # variant soon - - - - - - def on_team_join(self, team: Team) -> None: self._update_scoreboard() @@ -990,7 +1022,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): 'explosion', attrs={ 'position': self._puck.node.position, - 'color': (0,0,0), + 'color': (0, 0, 0), 'radius': .1, 'big': True, } @@ -1022,7 +1054,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): for player in team.players: if player.actor: - player.actor.handlemessage(bs.CelebrateMessage(2.0)) # yay we scored + player.actor.handlemessage(bs.CelebrateMessage(2.0)) # yay we scored player.actor.percentage = min(player.actor.percentage + 15, 100) # dont mind this ;( scorch = bs.newnode( @@ -1047,7 +1079,8 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): self._puck.scored = True # Kill the puck (it'll respawn itself shortly). - bs.animate(self._puck.node, 'mesh_scale', {0: 0.25, 0.4 : 0}) # Animation ! 0 sec = 0.25 of size in our case + # Animation ! 0 sec = 0.25 of size in our case + bs.animate(self._puck.node, 'mesh_scale', {0: 0.25, 0.4: 0}) bs.timer(0.7, self._kill_puck) bs.cameraflash(duration=7.0) @@ -1071,7 +1104,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # overriding the default character spawning.. def spawn_player(self, player: Player) -> bs.Actor: spaz = self.spawn_player_spaz(player) - spaz.bomb_count = 0 # No bombs for yall bahaha + spaz.bomb_count = 0 # No bombs for yall bahaha # Now we want to make that your punches are useless too # But we will need the button to work so let's put the damage to 0 and some custom stuff spaz._punch_power_scale = 0 @@ -1085,10 +1118,9 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): spaz.can_curve = False - spaz.last_angle = 0 spaz.total_rotation = 0.0 - spaz.rotation_direction = 0 + spaz.rotation_direction = 0 # 0: neutre, 1: clockwise, -1: not clockwise spaz.gliding = False spaz.smashed = False @@ -1106,7 +1138,7 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): ) spaz.node.connectattr('position', spaz.bar, 'position') spaz.bar.always_show_health_bar = True - spaz.node.name = '' # bahaha but you'll sspazuseful + spaz.node.name = '' # bahaha but you'll sspazuseful def check(): if spaz.node.exists(): @@ -1127,7 +1159,6 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): else: spaz.node.name = '' - spaz.bar.hurt = min(1.0 - float(spaz.percentage) / 100, 1) # Curved ball part @@ -1143,12 +1174,42 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): dx /= length dz /= length - spaz.angle = math.degrees(math.atan2(-dx, -dz)) # trust + spaz.angle = math.degrees(math.atan2(-dx, -dz)) # trust + # checks if u turn ! Not in the game yet ! + def check_270_rotation(): + now = bs.time() + current_angle = spaz.angle + delta = current_angle - spaz.last_angle + if delta > 180: + delta -= 360 + if delta < -180: + delta += 360 + current_dir = 1 if delta > 0 else -1 + + if spaz.rotation_direction != 0 and current_dir != spaz.rotation_direction: + spaz.total_rotation = 0 + + spaz.rotation_direction = current_dir + + spaz.total_rotation += abs(delta) + + if spaz.total_rotation >= 250: + spaz.curved = now + 0.40 + spaz.total_rotation = 0 + + spaz.last_angle = current_angle spaz.check_timer1 = bs.Timer(1/30, check, repeat=True) + # spaz.check_timer2 = bs.Timer(1/30, check_270_rotation, repeat=True) + + # spaz.node.hold_node = spaz.node ... nevermind + + # By the way when you have a node but want his player just do + # player = node.getdelegate(object) + # i dont know why you'll need it but here it is return spaz @@ -1164,17 +1225,17 @@ class VolleyBallGame(bs.TeamGameActivity[Player, Team]): # Respawn dead pucks. elif isinstance(msg, PuckDiedMessage): if not self.has_ended(): - bs.timer(2, self._spawn_puck) + bs.timer(2, self._spawn_puck) # 2 secs to breathe bahahaha else: super().handlemessage(msg) - def _spawn_puck(self) -> None: self._swipsound.play() self._whistle_sound.play() assert self._puck_spawn_pos is not None self._puck = Puck(position=self._puck_spawn_pos) - self.timer = bs.Timer(1 / 144, self.lock_it, repeat=True) + self.timer = bs.Timer(1 / 144, self.lock_it, repeat=True) # 144 ?.. yes + def lock_it(self): if self._puck is None: return @@ -1229,5 +1290,4 @@ class ModInstaller: bui.screenmessage("Installation Failed!", color=(1, 0, 0)) bui.getsound('kronk2').play() - bs.apptimer(2.5, _do_install) From d517d47f3edca4cf43f66b764c2d40ddf8734041 Mon Sep 17 00:00:00 2001 From: Scriptz1 <141498178+Scriptz1@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:26:09 -0400 Subject: [PATCH 62/84] Update volley_punch.py --- plugins/minigames/volley_punch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/minigames/volley_punch.py b/plugins/minigames/volley_punch.py index c19e269..f5260e4 100644 --- a/plugins/minigames/volley_punch.py +++ b/plugins/minigames/volley_punch.py @@ -36,7 +36,7 @@ plugman = dict( authors=[ {"name": "Learn.py", "email": "phillipians36@gmail.com", "discord": "Learning .py"}, ], - version="1.0.0", + version="1.1.0", ) From a6d11fd8ce9dc90965207b99ee61cdf36c1db36e Mon Sep 17 00:00:00 2001 From: Scriptz1 <141498178+Scriptz1@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:26:59 +0000 Subject: [PATCH 63/84] [ci] apply-plugin-metadata-and-formatting --- plugins/minigames.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/minigames.json b/plugins/minigames.json index 9ba5818..729cac0 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1675,6 +1675,7 @@ } ], "versions": { + "1.1.0": null, "1.0.0": { "api_version": 9, "commit_sha": "576a9c0", From d35b78326bc60980a5b44846e5213481072d7b9d Mon Sep 17 00:00:00 2001 From: Scriptz1 <141498178+Scriptz1@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:27:00 +0000 Subject: [PATCH 64/84] [ci] apply-version-metadata --- plugins/minigames.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index 729cac0..52bcd96 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1675,7 +1675,12 @@ } ], "versions": { - "1.1.0": null, + "1.1.0": { + "api_version": 9, + "commit_sha": "a6d11fd", + "released_on": "02-06-2026", + "md5sum": "a2d73ebe4cb84b723242c187ad5b5fbb" + }, "1.0.0": { "api_version": 9, "commit_sha": "576a9c0", From 1662980eeaad419aef1b95236bca8101ea810bb3 Mon Sep 17 00:00:00 2001 From: FluffyPal <149132594+FluffyPal0@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:12:31 +0800 Subject: [PATCH 65/84] Adding fluffyplaylisteditor --- plugins/utilities/fluffyplaylisteditor.py | 2104 +++++++++++++++++++++ 1 file changed, 2104 insertions(+) create mode 100644 plugins/utilities/fluffyplaylisteditor.py diff --git a/plugins/utilities/fluffyplaylisteditor.py b/plugins/utilities/fluffyplaylisteditor.py new file mode 100644 index 0000000..1d528f2 --- /dev/null +++ b/plugins/utilities/fluffyplaylisteditor.py @@ -0,0 +1,2104 @@ +# ba_meta require api 9 + +plugman = dict( + plugin_name="fluffyplaylisteditor", + description="A simple not-so advanced Playlist Editor", + external_url="https://discord.com/channels/1001896771347304639/1483463979450896445", + authors=[ + {"name": "FluffyPal", "email": "", "discord": "fluffypal"} + ], + version="1.1.0", +) + +### Started: 12 Mar 2026 at night from scratch +# - Learning workflow +# - configuring packages +# - PlaylistEditWindow UI Polishing (I HATE UI POLISHING) + +### Continued: 13 Mar 2026 +# - PlaylistEditGameWindow UI Polishing (AAAA.. oh, found a bug) +# - Implemented GameEdit configs reset and restore +# - Implemented GameEdit map quick navigation buttons +# - Lazy TypedDict PlaylistType + +### Continued: 17 Mar 2026 +# - PlaylistEditController logics overwrite +# - Implemented PlaylistEdit duplicate game +# - Refactor filter_playlist +# - Implemented logics to show invalid games/maps in PlaylistEdit +# - Refine GameEdit reset and restore + +### Continued: 7 June 2026 +# - Fixed map select not recorded on MapSelect +# - Added batch game adding within supported maps + + +import logging +from copy import deepcopy +from random import choice #, randint, randrange + + +import bauiv1 as bui +import bascenev1 as bs + +import babase +from babase import app, Plugin + + +#======= PLAYLIST PARENT =======# +from bascenev1._playlist import filter_playlist #, PlaylistType +from bauiv1lib.playlist import PlaylistTypeVars + +#======= PLAYLIST BORWSER =======# +# Main Playlist Browser Window For Starting The Playlist +# Then goes to PlaylistCustomizeBrowserWindow if pressing "customize..." +#from bauiv1lib.playlist.browser import PlaylistBrowserWindow + +#import bauiv1lib.playlist.customizebrowser +#from bauiv1lib.playlist.customizebrowser import PlaylistCustomizeBrowserWindow + +#======= PLAYLIST ADDING/EDITING =======# +## Main packages to modify +#- Game edit window +import bauiv1lib.playlist.edit +from bauiv1lib.playlist.edit import PlaylistEditWindow + +#from bauiv1lib.playlist.addgame import PlaylistAddGameWindow + +#- Playlist games edit window +import bauiv1lib.playlist.editgame +from bauiv1lib.playlist.editgame import PlaylistEditGameWindow + +# Window for adding registered games to the playlist and then goes to PlaylistEditController for editing +# The UI that shows games GameActivities names on the left and desc on the right +# And "Get More Games..." on the bottom +# After selecting the game, we goes to PlaylistEditGameWindow for configuring the game Settings and Map +import bauiv1lib.playlist.editcontroller +from bauiv1lib.playlist.editcontroller import PlaylistEditController + +#- Game map select window +#import bauiv1lib.playlist.mapselect +#from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow + + +#======= PLAYLIST PLAYING =======# +# This is used when we wanna "play" the playlist +#from bauiv1lib.play import PlayWindow, PlaylistSelectContext + +#from bauiv1lib.playoptions import PlayOptionsWindow + + + +#======= UI Packages =======# +from babase import ( + get_virtual_screen_size +) +from bauiv1 import ( + # Window utils + Window, MainWindowState, + MainWindow, + UIScale, + + # Widget utils + Widget, widget, + containerwidget, + scrollwidget, + columnwidget, + buttonwidget as original_buttonwidget, + textwidget, + imagewidget, + checkboxwidget, + get_special_widget, + # Other + gettexture, + Mesh, Texture, Lstr, +) +from bauiv1lib.confirm import ConfirmWindow + +#======= Type Hints =======# +#from enum import Enum +from typing import Callable, Sequence, Literal, TypedDict, Any, cast + + +class UntotalOldTypedPlaylistDict(TypedDict, total=False): + map: str + level: str + resolved_type: type[bs.GameActivity] | None + is_unowned_map: bool + is_unowned_game: bool + is_map_invalid: bool + is_game_invalid: bool + +class OldTypedPlaylistDict(TypedDict): + map: str + level: str + +class TypedPlaylistDict(UntotalOldTypedPlaylistDict): + settings: OldTypedPlaylistDict + type: str + +TypedPlaylistType = list[TypedPlaylistDict] + +#-------------------------- UI TOOLS --------------------------# +def buttonwidget( + *, + edit: Widget | None = None, + parent: Widget | None = None, + id: str | None = None, + size: Sequence[float] | None = None, + position: Sequence[float] | None = None, + on_activate_call: Callable | None = None, + label: str | Lstr | None = None, + color: Sequence[float] | None = None, + down_widget: Widget | None = None, + up_widget: Widget | None = None, + left_widget: Widget | None = None, + right_widget: Widget | None = None, + #texture: Texture | None = None, + text_scale: float | None = None, + textcolor: Sequence[float] | None = None, + enable_sound: bool | None = None, + mesh_transparent: Mesh | None = None, + mesh_opaque: Mesh | None = None, + repeat: bool | None = None, + scale: float | None = None, + transition_delay: float | None = None, + on_select_call: Callable | None = None, + button_type: str | None = None, + extra_touch_border_scale: float | None = None, + selectable: bool | None = None, + show_buffer_top: float | None = None, + icon: Texture | None = None, + iconscale: float | None = None, + icon_tint: float | None = None, + icon_color: Sequence[float] | None = None, + autoselect: bool | None = None, + mask_texture: Texture | None = None, + tint_texture: Texture | None = None, + tint_color: Sequence[float] | None = None, + tint2_color: Sequence[float] | None = None, + text_flatness: float | None = None, + text_res_scale: float | None = None, + text_literal: bool | None = None, + opacity: float | None = None, + better_bg_fit: bool | None = None, +) -> Widget: + return original_buttonwidget( + edit=edit, + parent=parent, + id=id, + size=size, + position=position, + on_activate_call=on_activate_call, + label=label, + color=color, + down_widget=down_widget, + up_widget=up_widget, + left_widget=left_widget, + right_widget=right_widget, + texture=gettexture('white'), + text_scale=text_scale, + textcolor=textcolor, + enable_sound=enable_sound, + mesh_transparent=mesh_transparent, + mesh_opaque=mesh_opaque, + repeat=repeat, + scale=scale, + transition_delay=transition_delay, + on_select_call=on_select_call, + button_type=button_type, + extra_touch_border_scale=extra_touch_border_scale, + selectable=selectable, + show_buffer_top=show_buffer_top, + icon=icon, + iconscale=iconscale, + icon_tint=icon_tint, + icon_color=icon_color, + autoselect=autoselect, + mask_texture=mask_texture, + tint_texture=tint_texture, + tint_color=tint_color, + tint2_color=tint2_color, + text_flatness=text_flatness, + text_res_scale=text_res_scale, + text_literal=text_literal, + opacity=opacity, + better_bg_fit=better_bg_fit + ) + +def create_container(size: tuple[float, float], **kwargs) -> Widget: + """Create the main window with overlay.""" + # Container + p = containerwidget( + #parent=get_special_widget('overlay_stack'), + background=False, + #transition='in_scale', + size=size, + **kwargs + ) + x, y = get_virtual_screen_size() + + # Background + imagewidget( + parent=p, + texture=gettexture('white'), + size=(x*2, y*2), + position=(-x*0.5, 0-475), + opacity=0.55, + color=(0, 0, 0) + ) + + # Main Container View + border_size_range = 1.0075 + b = imagewidget( + texture=gettexture('white'), + parent=p, + size=(size[0]*border_size_range, size[1]*border_size_range), + tilt_scale=6, + color=tuple(col*1.5 for col in color_theme.get_color('primary')) + ) + f = imagewidget( # Foreground + texture=gettexture('white'), + parent=p, + size=size, + color=color_theme.get_color('bg') + ) + + border_size_offset = border_size_range-(border_size_range//border_size_range.real) + imagewidget( + edit=b, + position=tuple(pos*-border_size_offset for pos in f.center) + ) + + return p + +def create_confirm(callback: Callable, text: str) -> Callable: + return lambda: ConfirmWindow( + text=f"{text}?", + color=color_theme.get_color('primary'), # pyright: ignore[reportArgumentType] + action=callback + ) + +_SOUND_VOL = 1.2 +def play_error_sound(): + bui.getsound('error').play(_SOUND_VOL) + +def play_guncocking_sound(): + bui.getsound('gunCocking').play(_SOUND_VOL) + +def play_ding_sound(): + bui.getsound('ding').play(_SOUND_VOL) + +def play_powerdown_sound(): + bui.getsound('powerdown01').play(_SOUND_VOL) + +def play_shield_down_sound(): + bui.getsound('shieldDown').play(_SOUND_VOL) + +def play_shield_up_sound(): + bui.getsound('shieldUp').play(_SOUND_VOL) + +def play_deek_sound(): + bui.getsound(choice(['deek', 'deek2'])).play(_SOUND_VOL) + +def play_click_sound(): + bui.getsound('click01').play(_SOUND_VOL) + + +class ScreenmessageColors: + RED = (1, 0, 0) + GREEN = (0, 1, 0) + BS_GREEN = (0.6, 1, 0.6) + BLUE = (0, 0, 1) + ORANGE = (1, 0.5, 0) + YELLOW = (1, 1, 0) + PURPLE = (0.6, 0, 1) + CYAN = (0, 1, 1) + MAGENTA = (1, 0, 1) + WHITE = (1, 1, 1) + BLACK = (0, 0, 0) + GRAY = (0.5, 0.5, 0.5) + PINK = (1, 0.4, 0.7) + + +_COLOR_TYPE = Literal[ + 'bg', 'primary', 'secondary', 'tertiary', 'unknown' +] +class ColorTheme: + main_color: Sequence[float] = (0.8, 0.8, 0.8) + colors: dict[_COLOR_TYPE, Sequence[float]] = { + 'bg': tuple(col*0.1 for col in main_color), + 'primary': main_color, + 'secondary': tuple(col*0.2 for col in main_color), + 'tertiary': tuple(col*0.4 for col in main_color), + 'unknown': (main_color[0]*1.2, main_color[1], main_color[2]) + } + + def get_color(self, color: _COLOR_TYPE) -> Sequence[float]: + return self.colors[color] + + +color_theme = ColorTheme() + + +#-------------------------- MAIN --------------------------# +class FluffyPlaylistEditController(PlaylistEditController): + def __init__( + self, + sessiontype: type[bs.Session], + from_window: MainWindow, + *, + existing_playlist_name: str | None = None, + playlist: TypedPlaylistType | None = None, + playlist_name: str | None = None, + ): + appconfig = bui.app.config + + # Since we may be showing our map list momentarily, + # lets go ahead and preload all map preview textures. + if app.classic is not None: + app.classic.preload_map_preview_media() + + self._sessiontype = sessiontype + + self._editing_game = False + self._editing_game_type: type[bs.GameActivity] | None = None + self._pvars = PlaylistTypeVars(sessiontype) + self._existing_playlist_name = existing_playlist_name + self._config_name_full = self._pvars.config_name + ' Playlists' + + self._is_batch_add = False + self._pre_game_add_state: MainWindowState | None = None + self._pre_game_edit_state: MainWindowState | None = None + + # Make sure config exists. + if self._config_name_full not in appconfig: + appconfig[self._config_name_full] = {} + + self._selected_index = 0 + if existing_playlist_name: + self._name = existing_playlist_name + + # Filter out invalid games. + self._playlist = new_filter_playlist( + appconfig[self._pvars.config_name + ' Playlists'][ + existing_playlist_name + ], + sessiontype=sessiontype, + remove_unowned=False, + #add_resolved_type=True, # HACK: This would cause json error as we save a Python class which aren't serializable + name=existing_playlist_name, + print_exc=False + ) + self._edit_ui_selection = None + else: + if playlist is not None: + self._playlist = playlist + else: + self._playlist = [] + if playlist_name is not None: + self._name = playlist_name + else: + # Find a good unused name. + i = 1 + while True: + self._name = ( + self._pvars.default_new_list_name.evaluate() + + ((' ' + str(i)) if i > 1 else '') + ) + if ( + self._name + not in appconfig[self._pvars.config_name + ' Playlists'] + ): + break + i += 1 + + # Also we want it to start with 'add' highlighted since its empty + # and that's all they can do. + self._edit_ui_selection = 'add_button' + + editwindow = from_window.main_window_replace( + lambda: FluffyPlaylistEditWindow(editcontroller=self) + ) + assert editwindow is not None + + # Once we've set our start window, store the back state. We'll + # skip back to there once we're fully done. + self._back_state = editwindow.main_window_back_state + + + def duplicate_game_pressed(self) -> bool: + """Duplicate the currently selected game in the playlist and insert it just after.""" + if not self._playlist or self._selected_index is None: + bs.screenmessage("No game selected to duplicate", ScreenmessageColors.ORANGE) + play_error_sound() + return False + + base_entry = self._playlist[self._selected_index] + new_entry = deepcopy(base_entry) + + ins_index = self._selected_index + 1 + self._playlist.insert(ins_index, new_entry) + self._selected_index = ins_index + self._edit_ui_selection = None + + try: + cls = babase.getclass(new_entry['type'], bs.GameActivity) + name = cls.getname() + except (ImportError, AttributeError): + name = new_entry['type'] + + bs.screenmessage(f"Game '{name}' duplicated", ScreenmessageColors.BS_GREEN) + return True + + def toggle_epic_mode(self) -> bool: + if not self._playlist or self._selected_index is None: + bs.screenmessage("No game selected to toggle epic mode", ScreenmessageColors.ORANGE) + play_error_sound() + return False + + entry = self._playlist[self._selected_index] + if (epic_setting := 'Epic Mode') in entry['settings'] and isinstance(entry['settings'][epic_setting], bool): + self._playlist[self._selected_index]['settings'][epic_setting] = not entry['settings'][epic_setting] # pyright: ignore[reportGeneralTypeIssues] + else: + play_error_sound() + return False + return True + + def toggle_solo_mode(self) -> bool: + if not self._playlist or self._selected_index is None: + bs.screenmessage("No game selected to toggle solo mode", ScreenmessageColors.ORANGE) + play_error_sound() + return False + + entry = self._playlist[self._selected_index] + if (solo_setting := 'Solo Mode') in entry['settings'] and isinstance(entry['settings'][solo_setting], bool): + self._playlist[self._selected_index]['settings'][solo_setting] = not entry['settings'][solo_setting] # pyright: ignore[reportGeneralTypeIssues] + else: + play_error_sound() + return False + return True + + def reslove_default_settings_defs(self, settings: OldTypedPlaylistDict) -> list[bs.Setting]: + default_settings = list[bs.Setting]() + if (epic_mode := 'Epic Mode') in settings: + default_settings.append( + bs.BoolSetting(epic_mode, default=settings[epic_mode]) + ) + if (solo_mode := 'Solo Mode') in settings: + default_settings.append( + bs.BoolSetting(solo_mode, default=settings[solo_mode]) + ) + if (time_limit := 'Time Limit') in settings: + default_settings.append( + bs.IntChoiceSetting( + time_limit, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=settings[time_limit], + ) + ) + if (respawn_times := 'Respawn Times') in settings: + default_settings.append( + bs.FloatChoiceSetting( + respawn_times, + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=settings[respawn_times], + ) + ) + return default_settings + + def edit_game_pressed(self, from_window: MainWindow) -> None: + if not self._playlist: + return + + playlist = self._playlist[self._selected_index] + try: + cls = babase.getclass( + playlist['type'], + subclassof=bs.GameActivity + ) + except AttributeError as e: + bs.screenmessage(f'Can\'t edit game: {e}. Maybe try fix it?', ScreenmessageColors.YELLOW) + play_error_sound() + return + + except ModuleNotFoundError as e: + class FakeGameActivity(bs.GameActivity): + name = playlist['type'] + available_settings = self.reslove_default_settings_defs(playlist['settings']) + cls = FakeGameActivity + + self._show_edit_ui( + gametype=cls, + settings=playlist['settings'], # pyright: ignore[reportArgumentType] + from_window=from_window, + ) + + def _show_edit_ui( # pyright: ignore[reportIncompatibleMethodOverride] + self, + gametype: type[bs.GameActivity], + settings: TypedPlaylistDict | None, + from_window: bui.MainWindow, + ) -> None: + # pylint: disable=cyclic-import + if not from_window.main_window_has_control(): + return + + self._editing_game = settings is not None + self._editing_game_type = gametype + assert self._sessiontype is not None + + # Jump into an edit window. + editwindow = from_window.main_window_replace( + lambda: FluffyPlaylistEditGameWindow( + gametype, + self._sessiontype, + settings, + completion_call=self._edit_game_done, + ) + ) + assert editwindow is not None + + # Once we're there, store the back state. We'll use that to jump + # back out to our current location once the edit is done. + assert self._pre_game_edit_state is None + self._pre_game_edit_state = editwindow.main_window_back_state + + def _edit_game_done( # pyright: ignore[reportIncompatibleMethodOverride] + self, config: TypedPlaylistDict | None, from_window: bui.MainWindow + ) -> None: + """Called after finished editing/adding a game""" + # No-op if provided window isn't in charge. + if not from_window.main_window_has_control(): + return + + assert bui.app.classic is not None + if config is None: + play_powerdown_sound() + else: + # Make sure type is in there. + assert self._editing_game_type is not None + if self._editing_game: + playlist = self._playlist[self._selected_index] + config['type'] = playlist['type'] + else: + config['type'] = bui.get_type_name(self._editing_game_type) + + if self._editing_game: + self._playlist[self._selected_index] = deepcopy(config) + else: + # Add a new entry to the playlist. + insert_index = min( + len(self._playlist), self._selected_index + 1 + ) + + self._selected_index = insert_index + self._playlist.insert(insert_index, deepcopy(config)) + + if self._is_batch_add: + for _map in self._get_supported_maps_for_game(config, self._editing_game_type): + new_config = deepcopy(config) + new_config['map'] = _map + new_config['settings']['map'] = _map + self._playlist.insert(insert_index, new_config) + insert_index += 1 + + + play_guncocking_sound() + + # If we're adding, jump to before the add started. + # Otherwise jump to before the edit started. + assert ( + self._pre_game_edit_state is not None + or self._pre_game_add_state is not None + ) + if self._pre_game_add_state is not None: + from_window.main_window_back_state = self._pre_game_add_state + elif self._pre_game_edit_state is not None: + from_window.main_window_back_state = self._pre_game_edit_state + + from_window.main_window_back() + self._pre_game_edit_state = None + self._pre_game_add_state = None + self._is_batch_add = False + + + def _get_supported_maps_for_game(self, config: TypedPlaylistDict, gametype: type[bs.GameActivity]): + assert app.classic is not None + store = app.classic.store + + valid_maps = set(gametype.get_supported_maps(self._sessiontype)) + to_remove = set(store.get_unowned_maps()) + + if config_map := config.get('map') or (config.get('settings', {}) or {}).get('map'): + to_remove.add(config_map) + + valid_maps -= to_remove + + return sorted(valid_maps) + + + def batch_add_game_pressed(self, from_window: MainWindow): + self._is_batch_add = True + self.add_game_pressed(from_window) + + +class FluffyPlaylistEditWindow(PlaylistEditWindow): + def __init__( + self, + editcontroller: FluffyPlaylistEditController, + transition: str | None = 'in_right', + origin_widget: Widget | None = None, + ): + self._list_widgets: list[Widget] = [] + self._editcontroller = editcontroller + editcontroller._is_batch_add = False # HACK: Couldn't think of any clean idea :p + + self._r = 'editGameListWindow' + prev_selection: str | None = self._editcontroller.get_edit_ui_selection() + + ### Container Setup + uiscale = app.ui_v1.uiscale + is_small_ui = uiscale is UIScale.SMALL + # Keep a constant aspect ratio for all UIScale values. + # Let's use width:height (classic UI aspect) as a reference. + # We'll use a base width and scale it for each UI scale while maintaining the ratio. + base_width = 800 + aspect_ratio = 16 / 9 + self._width = base_width * 1.65 + + self._height = self._width / aspect_ratio + + # Adjust x_inset proportionally to width + self.x_inset = x_inset = int(self._width * (0.09 if is_small_ui else 0.02)) + self.yoffs = yoffs = -68 if is_small_ui else -15 + + # Round to integers for pixel positions + self._width = int(self._width) + self._height = int(self._height) + + MainWindow.__init__(self, # pyright: ignore[reportArgumentType] + root_widget=create_container( + size=(self._width, self._height), + scale=( + 1.20 if is_small_ui else + 0.80 if uiscale is UIScale.MEDIUM else + 0.60 + ), + toolbar_visibility=( + 'menu_minimal_no_back' + if is_small_ui + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, + ) + + ### Widgets/children + # Buttons + self.b_color = b_color = color_theme.get_color('primary') + self.b_textcolor = b_textcolor = color_theme.get_color('secondary') + + cancel_button = buttonwidget( + parent=self._root_widget, + position=(35 + x_inset, self._height - 50 + yoffs), + scale=0.8, + size=(175, 60), + autoselect=True, + color=(b_color[0]*1.2, *b_color[1:]), + textcolor=b_textcolor, + label=bui.Lstr(resource='cancelText'), + text_scale=1.2, + ) + save_button = buttonwidget( + parent=self._root_widget, + position=( + self._width - ((225 if is_small_ui else 255) + x_inset), + self._height - 50 + yoffs + ), + scale=0.8, + size=(190, 60), + autoselect=True, + color=(b_color[0], b_color[1]*1.2, b_color[2]), + textcolor=b_textcolor, + left_widget=cancel_button, + label=bui.Lstr(resource='saveText'), + text_scale=1.2, + ) + + widget( + edit=save_button, + right_widget=bui.get_special_widget('squad_button'), + ) + + widget( + edit=cancel_button, + left_widget=cancel_button, + right_widget=save_button, + ) + + v_gap = 60 + h = 40 + x_inset + r_h = x_inset * (9.45 if is_small_ui else 45.25) + + left_button_v_size = 45 + right_button_v_size = 45 + + v = self._height - 172.0 + yoffs + v *= 1.1 + v -= 2.0 + v -= v_gap + buttonwidget( + parent=self._root_widget, + position=(r_h, v), + size=(110, right_button_v_size), + on_activate_call=self._toggle_epic_mode, + enable_sound=False, + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label="Toggle\nEpic Mode", + ) + v -= v_gap + + buttonwidget( + parent=self._root_widget, + position=(r_h, v), + size=(110, right_button_v_size), + on_activate_call=self._toggle_solo_mode, + enable_sound=False, + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label="Toggle\nSolo Mode", + ) + v -= v_gap + + v = self._height - 172.0 + yoffs + v *= 1.1 + v -= 2.0 + v -= v_gap + + add_game_button = buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, left_button_v_size), + on_activate_call=self._add, + on_select_call=bui.CallPartial(self._set_ui_selection, 'add_button'), + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label=bui.Lstr(resource=f'{self._r}.addGameText'), + ) + v -= v_gap + + batch_add_game_button = buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, left_button_v_size), + on_activate_call=self._batch_add, + on_select_call=bui.CallPartial(self._set_ui_selection, 'batch_add_button'), + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label=f"Batch {bui.Lstr(resource=f'{self._r}.addGameText').evaluate()}", + ) + v -= v_gap + + self._edit_button = edit_game_button = buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, left_button_v_size), + on_activate_call=self._edit, + on_select_call=bui.CallPartial(self._set_ui_selection, 'editButton'), + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label=bui.Lstr(resource=f'{self._r}.editGameText'), + ) + v -= v_gap + + remove_game_button = buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, left_button_v_size), + text_scale=0.8, + on_activate_call=self._remove, + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + label=bui.Lstr(resource=f'{self._r}.removeGameText'), + ) + v -= v_gap + + # Customs + duplicate_button = buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, left_button_v_size), + on_activate_call=self._duplicate_selected_game, + autoselect=True, + enable_sound=False, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label="Duplicate\nGame", + ) + v -= v_gap - 10 + + h += 9 + buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(42, 35), + on_activate_call=self._move_up, + label=bui.charstr(bui.SpecialChar.UP_ARROW), + button_type='square', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + repeat=True, + ) + h += 52 + buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(42, 35), + on_activate_call=self._move_down, + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + label=bui.charstr(bui.SpecialChar.DOWN_ARROW), + repeat=True, + ) + + # Scroller + v = self._height - 100 + yoffs + scroll_height = self._height - ( + 250 if is_small_ui else 155 + ) + self._scroll_width = self._width - (205 + (2.5 if is_small_ui else 5.5) * x_inset) + + scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=(160 + x_inset, v - scroll_height), + highlight=False, + on_select_call=bui.CallStrict(self._set_ui_selection, 'gameList'), + size=(self._scroll_width, (scroll_height - 15)), + border_opacity=0.4, + ) + widget( + edit=scrollwidget, + left_widget=add_game_button, + right_widget=scrollwidget, + ) + self._columnwidget = columnwidget( + parent=scrollwidget, border=2, margin=0 + ) + + for button in [ + add_game_button, batch_add_game_button, edit_game_button, remove_game_button, duplicate_button + ]: + widget( + edit=button, left_widget=button, right_widget=scrollwidget + ) + + buttonwidget(edit=cancel_button, on_activate_call=self._cancel) + containerwidget( + edit=self._root_widget, + cancel_button=cancel_button, + selected_child=scrollwidget, + ) + + buttonwidget(edit=save_button, on_activate_call=self._save_press) + containerwidget(edit=self._root_widget, start_button=save_button) + + # Texts + textwidget( + parent=self._root_widget, + position=(-10, self._height - 50 + yoffs), + size=(self._width, 25), + text=bui.Lstr(resource=f'{self._r}.titleText'), + color=app.ui_v1.title_color, + scale=1.05, + h_align='center', + v_align='center', + maxwidth=270, + ) + + v = self._height - 115.0 + yoffs + + textwidget( + parent=self._root_widget, + text=bui.Lstr(resource=f'{self._r}.listNameText'), + position=(196 + x_inset, v + 31), + maxwidth=150, + color=(0.8, 0.8, 0.8, 0.5), + size=(0, 0), + scale=0.75, + h_align='right', + v_align='center', + ) + + self._text_field = text_field = textwidget( + parent=self._root_widget, + position=(210 + x_inset, v + 7), + size=(self._scroll_width - 53, 43), + text=self._editcontroller.getname(), + h_align='left', + v_align='center', + max_chars=40, + maxwidth=380, + autoselect=True, + color=(0.9, 0.9, 0.9, 1.0), + description=bui.Lstr(resource=f'{self._r}.listNameText'), + editable=True, + padding=4, + on_return_press_call=self._save_press_with_sound, + ) + widget(edit=self._columnwidget, up_widget=text_field) + widget(edit=cancel_button, down_widget=text_field) + widget(edit=add_game_button, up_widget=text_field) + widget(edit=batch_add_game_button, up_widget=add_game_button) + + if prev_selection == 'add_button': + containerwidget( + edit=self._root_widget, selected_child=add_game_button + ) + elif prev_selection == 'batch_add_button': + containerwidget( + edit=self._root_widget, selected_child=batch_add_game_button + ) + elif prev_selection == 'editButton': + containerwidget( + edit=self._root_widget, selected_child=edit_game_button + ) + elif prev_selection == 'gameList': + containerwidget( + edit=self._root_widget, selected_child=scrollwidget + ) + + self._refresh() + + def _get_invalid_game_name(self, pentry: TypedPlaylistDict): + name = pentry['type'] + # A few substitutions for 'Epic', 'Solo' etc. modes. + # FIXME: Should provide a way for game types to define filters of + # their own and should not rely on hard-coded settings names. + if (solo_mode := 'Solo Mode') in pentry['settings'] and pentry['settings'][solo_mode]: + name = babase.Lstr( + resource='soloNameFilterText', subs=[('${NAME}', name)] + ) + if (epic_mode := 'Epic Mode') in pentry['settings'] and pentry['settings'][epic_mode]: + name = babase.Lstr( + resource='epicNameFilterText', subs=[('${NAME}', name)] + ) + + # Resolve map name + if 'map' in pentry['settings']: + sval = babase.Lstr( + value='${NAME} @ ${MAP}', + subs=[ + ('${NAME}', name), + ('${MAP}', bs.get_map_display_string( + bs.get_filtered_map_name(pentry['settings']['map'])), + ), + ], + ) + elif 'map' in pentry: + sval = babase.Lstr( + value='${NAME} @ ${MAP}', + subs=[ + ('${NAME}', name), + ('${MAP}', bs.get_map_display_string( + bs.get_filtered_map_name(pentry['map'])), + ), + ], + ) + else: + print('invalid game config - expected map entry under settings') + sval = babase.Lstr(value='???') + + return sval + + + def _duplicate_selected_game(self) -> None: + if self._editcontroller.duplicate_game_pressed(): + play_guncocking_sound() + self._refresh() + + + def _move_down(self) -> None: + if len(self._editcontroller.get_playlist()) > 1: + super()._move_down() + + def _move_up(self) -> None: + if len(self._editcontroller.get_playlist()) > 1: + super()._move_up() + + + def _toggle_epic_mode(self): + if self._editcontroller.toggle_epic_mode(): + play_guncocking_sound() + self._refresh() + + + def _toggle_solo_mode(self): + if self._editcontroller.toggle_solo_mode(): + play_guncocking_sound() + self._refresh() + + + def _refresh(self) -> None: + # Need to grab this here as rebuilding the list will + # change it otherwise. + old_selection_index = self._editcontroller.get_selected_index() + + while self._list_widgets: + self._list_widgets.pop().delete() + for index, pentry in enumerate(self._editcontroller.get_playlist()): # pyright: ignore[reportAssignmentType] + pentry: TypedPlaylistDict + try: + cls = babase.getclass(pentry['type'], subclassof=bs.GameActivity) + desc = cls.get_settings_display_string(pentry) # pyright: ignore[reportArgumentType] + color = (0.8, 0.8, 0.8, 1.0) + except Exception: + #logging.exception('Error in playlist refresh.') + #desc = "(invalid: '" + pentry['type'] + "')" + desc = self._get_invalid_game_name(pentry) + color = color_theme.get_color('unknown') + + txtw = textwidget( + parent=self._columnwidget, + size=(self._width - 80, 30), + on_select_call=bui.CallStrict(self._select, index), + always_highlight=True, + color=color, + padding=0, + maxwidth=self._scroll_width * 0.93, + text=desc, + on_activate_call=self._edit_button.activate, + v_align='center', + selectable=True, + ) + widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) + + # Wanna be able to jump up to the text field from the top one. + if index == 0: + widget(edit=txtw, up_widget=self._text_field) + self._list_widgets.append(txtw) + if old_selection_index == index: + columnwidget( + edit=self._columnwidget, + selected_child=txtw, + visible_child=txtw, + ) + + def _batch_add(self) -> None: + # Store list name then tell the session to perform an add. + self._editcontroller.setname( + cast(str, bui.textwidget(query=self._text_field)) + ) + self._editcontroller.batch_add_game_pressed(from_window=self) + + +from bascenev1 import ( + get_filtered_map_name, + get_map_class, + get_map_display_string, +) +class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): + + def __init__( + self, + gametype: type[bs.GameActivity], + sessiontype: type[bs.Session], + config: TypedPlaylistDict | None, + completion_call: Callable[[TypedPlaylistDict | None, bui.MainWindow], Any], + default_selection: str | None = None, + transition: str | None = 'in_right', + origin_widget: bui.Widget | None = None, + edit_info: dict[str, Any] | None = None + ): + + assert app.classic is not None + store = app.classic.store + + self._scrollwidget: Widget | None = None + self._subcontainer: Widget | None = None + + self._gametype = gametype + self._sessiontype = sessiontype + + # If we're within an editing session we get passed edit_info + # (returning from map selection window, etc). + if edit_info is not None: + self._edit_info = edit_info + + # ..otherwise determine whether we're adding or editing a game based + # on whether an existing config was passed to us. + else: + if config is None: + self._edit_info = {'editType': 'add'} + else: + self._edit_info = {'editType': 'edit'} + + self._r = 'gameSettingsWindow' + + self._valid_maps = valid_maps = gametype.get_supported_maps(sessiontype) + if not valid_maps: + bui.screenmessage(bui.Lstr(resource='noValidMapsErrorText')) + raise RuntimeError('No valid maps found.') + self._valid_maps_owned = [m for m in self._valid_maps if m not in store.get_unowned_maps()] + + self._config = config + + self._settings_defs = gametype.get_available_settings(sessiontype) + self._completion_call = completion_call + + # If there's a valid map name in the existing config, use that. + self._map: str | None = None + # To start with, pick a random map out of the ones we own. + unowned_maps = store.get_unowned_maps() + try: + if ( + config is not None + and 'map' in config + ): + filtered_map_name = get_filtered_map_name( + config['map'] + ) + if filtered_map_name not in unowned_maps: + self._map = filtered_map_name + elif ( + config is not None + and (settings := config.get('settings')) + and (raw_map := settings.get('map')) + ): + filtered_map_name = get_filtered_map_name(raw_map) + if filtered_map_name not in unowned_maps: + self._map = filtered_map_name + #else: + # raise Exception() + except Exception: + logging.exception('Error getting map for editor.') + + if not self._map: + if valid_maps_owned := [m for m in valid_maps if m not in unowned_maps]: + self._map = choice(valid_maps_owned) + # Hmmm.. we own none of these maps.. just pick a random un-owned one + # I guess.. should this ever happen? + else: + self._map = choice(valid_maps) + + if config is not None: + if 'settings' in config: + self._settings = config['settings'] + else: + self._settings = config + else: + self._settings: OldTypedPlaylistDict = {} # pyright: ignore[reportAttributeAccessIssue] + self._settings['map'] = self._map + + self._default_settings = deepcopy(self._settings) + + try: + self.map_tex_name = get_map_class(self._map).get_preview_texture_name() + except babase.NotFoundError: + self.map_tex_name = 'null' + + if self.map_tex_name is None: + raise RuntimeError(f'No map preview tex found for {self._map}.') + self._choice_selections: dict[str, int] = {} + + ### Container Setup + uiscale = app.ui_v1.uiscale + is_small_ui = uiscale is UIScale.SMALL + # Keep a constant aspect ratio for all UIScale values. + # Let's use width:height (classic UI aspect) as a reference. + # We'll use a base width and scale it for each UI scale while maintaining the ratio. + base_width = 900 + aspect_ratio = 5 / 4 + self._width = width = int(base_width * 1.65) + self._height = height = int(base_width / aspect_ratio) + + y_extra2 = 50 # For topper widget elements + + # Adjust x_inset proportionally to width + self.x_inset = int(width * (0.09 if is_small_ui else 0.0225)) + self.yoffs = yoffs = -68 if is_small_ui else -30 + + MainWindow.__init__(self, + root_widget=containerwidget( + size=(width, height), + color=color_theme.get_color('bg'), + scale=( + 1.10 if is_small_ui else + 0.80 if uiscale is UIScale.MEDIUM else + 0.70 + ), + toolbar_visibility=( + 'menu_minimal_no_back' + if uiscale is UIScale.SMALL + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, + ) + + ### Widgets/children + b_color = color_theme.get_color('primary') + b_textcolor = color_theme.get_color('secondary') + + is_add = self._edit_info['editType'] == 'add' + cancel_button = original_buttonwidget( + parent=self._root_widget, + position=(45 + self.x_inset, height - 82 + y_extra2 + yoffs), + size=(60, 48) if is_add else (180, 65), + label=( + bui.charstr(bui.SpecialChar.BACK) if is_add else + bui.Lstr(resource='cancelText') + ), + button_type='backSmall' if is_add else None, + autoselect=True, + scale=1.0 if is_add else 0.75, + text_scale=1.3, + color=(b_color[0]*1.2, *b_color[1:]), + textcolor=b_textcolor, + on_activate_call=bui.CallStrict(self._cancel), + ) + containerwidget(edit=self._root_widget, cancel_button=cancel_button) + + # Title + textwidget( + parent=self._root_widget, + position=((-20 if is_small_ui else -18), height - 70 + y_extra2 + yoffs), + size=(width, 25), + text=gametype.get_display_string(), + color=bui.app.ui_v1.title_color, + maxwidth=width*0.35, + scale=1.1, + h_align='center', + v_align='center', + ) + + self.add_button = add_button = original_buttonwidget( + parent=self._root_widget, + position=(width - ((255 if is_small_ui else 235) + self.x_inset), height - 82 + y_extra2 + yoffs), + size=(200, 65), + scale=0.75, + text_scale=1.3, + color=(b_color[0], b_color[1]*1.2, b_color[2]), + textcolor=b_textcolor, + label=( + bui.Lstr(resource=f'{self._r}.addGameText') if is_add else + bui.Lstr(resource='applyText') + ), + ) + + base_h_pos = width - ((155 if is_small_ui else 135) + self.x_inset) + base_v_pos = height * 0.75 + right_buttons_gap = 75 + + reset_config_text = "Reset\nSettings" + original_buttonwidget( + parent=self._root_widget, + position=(base_h_pos, base_v_pos - right_buttons_gap), + size=(200, 100), + scale=0.75, + text_scale=1.3, + color=b_color, + textcolor=b_textcolor, + label=reset_config_text, + on_activate_call=create_confirm(self._reset_settings, reset_config_text.replace('\n', ' ')), + icon=gettexture('replayIcon'), + iconscale=1.5 + ) + right_buttons_gap += right_buttons_gap + + restore_config_text = "Restore\nSettings" + original_buttonwidget( + parent=self._root_widget, + position=(base_h_pos, base_v_pos - right_buttons_gap), + size=(200, 100), + scale=0.75, + text_scale=1.3, + color=b_color, + textcolor=b_textcolor, + label=restore_config_text, + on_activate_call=( + create_confirm(self._restore_settings, restore_config_text.replace('\n', ' ')) if not is_add else play_error_sound + ), + icon=gettexture('leftButton'), + iconscale=1.5 + ) + + self._refresh_settings_items() + + original_buttonwidget( + edit=add_button, on_activate_call=bui.CallStrict(self._add) + ) + containerwidget( + edit=self._root_widget, + selected_child=add_button, + start_button=add_button, + ) + + if default_selection == 'map': + containerwidget( + edit=self._root_widget, selected_child=self._scrollwidget + ) + containerwidget( + edit=self._subcontainer, selected_child=self._map_buttonwidget + ) + + # Tools + def _reset_settings(self): + updated = False + if self._settings: + for setting in self._settings_defs: + if (value := setting.default) != (data := self._settings)[setting.name]: + data[setting.name] = value + updated = True + + if (def_map := self._default_settings['map']) != self._map: + self._map = def_map + try: + self.map_tex_name = get_map_class(def_map).get_preview_texture_name() + except babase.NotFoundError: + self.map_tex_name = 'null' + updated = True + + if updated: + self._refresh_settings_items() + play_shield_down_sound() + else: + play_error_sound() + + def _restore_settings(self): + updated = False + # Only update keys which are different, and track if anything actually changed + for key, value in self._default_settings.items(): + if key not in self._settings or self._settings[key] != value: + #print(f'key restored: {self._settings[key]} -> {key}') + self._settings[key] = value + updated = True + + # Check if map changed, update relevant attrs + restored_map = get_filtered_map_name(self._settings['map']) + if self._map != restored_map: + #print(f'map restored: {self._map} -> {restored_map}') + self._map = restored_map + try: + self.map_tex_name = get_map_class(self._map).get_preview_texture_name() + except babase.NotFoundError: + self.map_tex_name = 'null' + updated = True + + if updated: + self._refresh_settings_items() + self._update_map_widget() + play_ding_sound() + else: + play_error_sound() + + def _randomize_map(self): + valid_maps = self._valid_maps_owned + + # Hmmm.. we own none of these maps.. just pick a random un-owned one + # I guess.. should this ever happen? + cur_map = choice(valid_maps) + self._map = cur_map + self.map_tex_name = get_map_class(cur_map).get_preview_texture_name() + self._update_map_widget() + + play_deek_sound() + + def _update_map_widget(self): + assert self.map_tex_name and self._map + imagewidget( + edit=self._map_imagewidget, + texture=gettexture(self.map_tex_name) + ) + textwidget( + edit=self._map_textwidget, + text=get_map_display_string(self._map) + ) + + def _shift_selected_map(self, index: int): + """Shift selection in the valid maps list by target (wrap around)""" + valid_maps = self._valid_maps_owned + + cur_map = self._map; assert cur_map + try: + cur_map_index = valid_maps.index(cur_map) + except ValueError: + cur_map_index = 0 + new_index = (cur_map_index + index) % len(valid_maps) + + self._map = valid_maps[new_index] + self.map_tex_name = get_map_class(self._map).get_preview_texture_name() + + self._update_map_widget() + play_click_sound() + + def _refresh_settings_items(self): + uiscale = app.ui_v1.uiscale + is_small_ui = uiscale is UIScale.SMALL + + pbtn = get_special_widget('squad_button') + widget(edit=self.add_button, right_widget=pbtn, up_widget=pbtn) + + map_height = 100 + + scroll_width = self._width - (86 + (3 if is_small_ui else 5.5) * self.x_inset) + + spacing = 47 # Scroller playlist config items spacing + y_extra = 15 # For scroller widget + + # Calc our total height we'll need + scroll_height = map_height + 10 # map select and margin + scroll_height += spacing * len(self._settings_defs) + + if not self._scrollwidget: + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=( + 44 + self.x_inset, + (95 if uiscale is UIScale.SMALL else 55) + y_extra + self.yoffs, + ), + size=( + scroll_width, + self._height - (166 if uiscale is UIScale.SMALL else 116), + ), + highlight=False, + claims_left_right=True, + selection_loops_to_parent=True, + border_opacity=0.4, + ) + if self._subcontainer: + for child in self._subcontainer.get_children(): + child.delete() + self._subcontainer.delete() + + self._subcontainer = containerwidget( + parent=self._scrollwidget, + size=(scroll_width, scroll_height), + background=False, + claims_left_right=True, + selection_loops_to_parent=True, + ) + + v = scroll_height - 5 + h = -40 + + # Keep track of all the selectable widgets we make so we can wire + # them up conveniently. + widget_column: list[list[bui.Widget]] = [] + + b_color = color_theme.get_color('primary') + b_textcolor = color_theme.get_color('secondary') + + textwidget( + parent=self._subcontainer, + position=(h + 49, v - 63), + size=(100, 30), + maxwidth=110, + text=bui.Lstr(resource='mapText'), + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center', + ) + assert self.map_tex_name + map_tex = gettexture(self.map_tex_name) + + self._map_imagewidget = imagewidget( + parent=self._subcontainer, + size=(256 * 0.7, 125 * 0.7), + position=(h + scroll_width * 0.46, v - 90), + texture=map_tex, + mesh_opaque=bui.getmesh('level_select_button_opaque'), + mesh_transparent=bui.getmesh('level_select_button_transparent'), + mask_texture=gettexture('mapPreviewMask'), + ) + + self._map_buttonwidget = None + if len(self._valid_maps_owned) > 1: + original_buttonwidget( # map_prev + parent=self._subcontainer, + position=(h + scroll_width * 0.46 - 50 - 1, v - 63), + size=(35, 35), + label='<', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallPartial(self._shift_selected_map, -1), + enable_sound=False, + repeat=True, + ) + original_buttonwidget( # map_next + parent=self._subcontainer, + position=(h + scroll_width * (0.655 if is_small_ui else 0.6225) + 5, v - 63), + size=(35, 35), + label='>', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallPartial(self._shift_selected_map, 1), + enable_sound=False, + repeat=True, + ) + original_buttonwidget( + parent=self._subcontainer, + size=(140, 60), + position=(h + scroll_width * (0.775 if is_small_ui else 0.8), v - 72), + on_activate_call=bui.CallStrict(self._randomize_map), + enable_sound=False, + color=b_color, + textcolor=b_textcolor, + scale=0.7, + label="Randomize", + ) + # Map select button. + self._map_buttonwidget = original_buttonwidget( + parent=self._subcontainer, + size=(140, 60), + position=(h + scroll_width * 0.9, v - 72), + on_activate_call=bui.CallStrict(self._select_map), + scale=0.7, + color=b_color, + textcolor=b_textcolor, + label=bui.Lstr(resource='mapSelectText'), + ) + widget_column.append([self._map_buttonwidget]) + + assert self._map + self._map_textwidget = textwidget( + parent=self._subcontainer, + position=(h + scroll_width * 0.496, v - 114), + size=(100, 30), + flatness=1.0, + shadow=1.0, + scale=0.55, + maxwidth=256 * 0.7 * 0.8, + text=get_map_display_string(self._map), + h_align='center', + color=(0.6, 1.0, 0.6, 1.0), + v_align='center', + ) + v -= map_height + + config = self._settings; assert config + for setting in self._settings_defs: + value = setting.default + value_type = type(value) + + # Now, if there's an existing value for it in the config, + # override with that. + try: + if config is not None: + if ( + 'settings' in config + and (seeting_name := setting.name) in config['settings'] + ): + value = value_type(config['settings'][seeting_name]) + elif (seeting_name := setting.name) in config: + value = value_type(config[seeting_name]) + except Exception: + logging.exception('Error getting game setting.') + + # Shove the starting value in there to start. + self._settings[setting.name] = value + + name_translated = self._get_localized_setting_name(setting.name) + + mw1 = 280 + mw2 = 70 + + # Handle types with choices specially: + item_h_pos = h + scroll_width * 0.96 + if isinstance(setting, bs.ChoiceSetting): + invalid = False + for choice in setting.choices: + if len(choice) != 2: + raise ValueError( + "Expected 2-member tuples for 'choices'; got: " + + repr(choice) + ) + if not isinstance(choice[0], str): + raise TypeError( + 'First value for choice tuple must be a str; got: ' + + repr(choice) + ) + if not isinstance(choice[1], value_type): + invalid = True + #raise TypeError( + # 'Choice type does not match default value; choice:' + # + repr(choice) + # + '; setting:' + # + repr(setting) + #) + if value_type not in (int, float): + raise TypeError( + 'Choice type setting must have int or float default; ' + 'got: ' + repr(setting) + ) + + # Start at the choice corresponding to the default if possible. + self._choice_selections[setting.name] = 0 + for index, choice in enumerate(setting.choices): + if choice[1] == value: + self._choice_selections[setting.name] = index + break + + v -= spacing + textwidget( + parent=self._subcontainer, + position=(h + 50, v), + size=(100, 30), + maxwidth=mw1, + text=name_translated, + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center', + ) + txt = textwidget( + parent=self._subcontainer, + position=(item_h_pos - 95, v), + size=(0, 28), + text=self._get_localized_setting_name( + setting.choices[self._choice_selections[setting.name]][ + 0 + ] + ), + editable=False, + color=(0.6, 1.0, 0.6, 1.0), + maxwidth=mw2, + h_align='right', + v_align='center', + padding=2, + ) + btn1 = original_buttonwidget( + parent=self._subcontainer, + position=(item_h_pos - 50 - 1, v), + size=(28, 28), + label='<', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallStrict( + self._choice_inc, setting.name, txt, setting, -1 + ) if not invalid else lambda: play_error_sound(), + repeat=True, + ) + btn2 = original_buttonwidget( + parent=self._subcontainer, + position=(item_h_pos + 5, v), + size=(28, 28), + label='>', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallStrict( + self._choice_inc, setting.name, txt, setting, 1 + ) if not invalid else lambda: play_error_sound(), + repeat=True, + ) + widget_column.append([btn1, btn2]) + + elif isinstance(setting, (bs.IntSetting, bs.FloatSetting)): + v -= spacing + min_value = setting.min_value + max_value = setting.max_value + increment = setting.increment + textwidget( + parent=self._subcontainer, + position=(h + 50, v), + size=(100, 30), + text=name_translated, + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center', + maxwidth=mw1, + ) + txt = textwidget( + parent=self._subcontainer, + position=(item_h_pos - 95, v), + size=(0, 28), + text=str(value), + editable=False, + color=(0.6, 1.0, 0.6, 1.0), + maxwidth=mw2, + h_align='right', + v_align='center', + padding=2, + ) + btn1 = original_buttonwidget( + parent=self._subcontainer, + position=(item_h_pos - 50 - 1, v), + size=(28, 28), + label='-', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallStrict( + self._inc, + txt, + min_value, + max_value, + -increment, + value_type, + setting.name, + ), + repeat=True, + ) + btn2 = original_buttonwidget( + parent=self._subcontainer, + position=(item_h_pos + 5, v), + size=(28, 28), + label='+', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + on_activate_call=bui.CallStrict( + self._inc, + txt, + min_value, + max_value, + increment, + value_type, + setting.name, + ), + repeat=True, + ) + widget_column.append([btn1, btn2]) + + elif value_type == bool: + v -= spacing + textwidget( + parent=self._subcontainer, + position=(h + 50, v), + size=(100, 30), + text=name_translated, + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center', + maxwidth=mw1, + ) + txt = textwidget( + parent=self._subcontainer, + position=(item_h_pos - 95, v), + size=(0, 28), + text=( + bui.Lstr(resource='onText') + if value + else bui.Lstr(resource='offText') + ), + editable=False, + color=(0.6, 1.0, 0.6, 1.0), + maxwidth=mw2, + h_align='right', + v_align='center', + padding=2, + ) + cbw = checkboxwidget( + parent=self._subcontainer, + text='', + position=(item_h_pos - 50 - 5, v - 2), + size=(200, 30), + autoselect=True, + color=b_color, + textcolor=b_textcolor, + value=value, + on_value_change_call=bui.CallPartial( + self._check_value_change, setting.name, txt + ), + ) + widget_column.append([cbw]) + + else: + raise TypeError(f'Invalid value type: {value_type}.') + + # Ok now wire up the column. + try: + prev_widgets: list[Widget] | None = None + for cwdg in widget_column: + if prev_widgets is not None: + # Wire our rightmost to their rightmost. + widget(edit=prev_widgets[-1], down_widget=cwdg[-1]) + widget(edit=cwdg[-1], up_widget=prev_widgets[-1]) + + # Wire our leftmost to their leftmost. + widget(edit=prev_widgets[0], down_widget=cwdg[0]) + widget(edit=cwdg[0], up_widget=prev_widgets[0]) + prev_widgets = cwdg + except Exception: + logging.exception( + 'Error wiring up game-settings-select widget column.' + ) + + +original_filter_playlist = filter_playlist +def new_filter_playlist( + playlist: TypedPlaylistDict, + sessiontype: type[bs.Session], + *, + add_resolved_type: bool = False, + remove_unowned: bool = True, + mark_unowned: bool = False, + name: str = '?', + print_exc: bool = True +) -> TypedPlaylistType: + """Return a filtered version of a playlist. + + Strips out or replaces invalid or unowned game types, makes sure all + settings are present, and adds in a 'resolved_type' which is the actual + type. + """ + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + from bascenev1 import get_filtered_map_name, GameActivity + + assert app.classic is not None + + goodlist = TypedPlaylistType() + available_maps: list[str] = list(app.classic.maps.keys()) + if (remove_unowned or mark_unowned) and app.classic is not None: + unowned_maps = app.classic.store.get_unowned_maps() + unowned_game_types = app.classic.store.get_unowned_game_types() + else: + unowned_maps = [] + unowned_game_types = set() + + for entry in deepcopy(playlist): # pyright: ignore[reportAssignmentType] + entry: TypedPlaylistDict + # 'map' used to be called 'level' here. + if 'level' in entry: + entry['map'] = entry['level'] + del entry['level'] + + # We now stuff map into settings instead of it being its own thing. + if 'map' in entry: + entry['settings']['map'] = entry['map'] + del entry['map'] + + # Update old map names to new ones. + entry['settings']['map'] = get_filtered_map_name( + entry['settings']['map'] + ) + if remove_unowned and entry['settings']['map'] in unowned_maps: + continue + + # Ok, for each game in our list, try to import the module and grab + # the actual game class. add successful ones to our initial list + # to present to the user. + if not isinstance(entry['type'], str): + raise TypeError('invalid entry format') + try: + # Do some type filters for backwards compat. + if entry['type'] in ( + 'Assault.AssaultGame', + 'Happy_Thoughts.HappyThoughtsGame', + 'bsAssault.AssaultGame', + 'bs_assault.AssaultGame', + 'bastd.game.assault.AssaultGame', + ): + entry['type'] = 'bascenev1lib.game.assault.AssaultGame' + if entry['type'] in ( + 'King_of_the_Hill.KingOfTheHillGame', + 'bsKingOfTheHill.KingOfTheHillGame', + 'bs_king_of_the_hill.KingOfTheHillGame', + 'bastd.game.kingofthehill.KingOfTheHillGame', + ): + entry['type'] = ( + 'bascenev1lib.game.kingofthehill.KingOfTheHillGame' + ) + if entry['type'] in ( + 'Capture_the_Flag.CTFGame', + 'bsCaptureTheFlag.CTFGame', + 'bs_capture_the_flag.CTFGame', + 'bastd.game.capturetheflag.CaptureTheFlagGame', + ): + entry['type'] = ( + 'bascenev1lib.game.capturetheflag.CaptureTheFlagGame' + ) + if entry['type'] in ( + 'Death_Match.DeathMatchGame', + 'bsDeathMatch.DeathMatchGame', + 'bs_death_match.DeathMatchGame', + 'bastd.game.deathmatch.DeathMatchGame', + ): + entry['type'] = 'bascenev1lib.game.deathmatch.DeathMatchGame' + if entry['type'] in ( + 'ChosenOne.ChosenOneGame', + 'bsChosenOne.ChosenOneGame', + 'bs_chosen_one.ChosenOneGame', + 'bastd.game.chosenone.ChosenOneGame', + ): + entry['type'] = 'bascenev1lib.game.chosenone.ChosenOneGame' + if entry['type'] in ( + 'Conquest.Conquest', + 'Conquest.ConquestGame', + 'bsConquest.ConquestGame', + 'bs_conquest.ConquestGame', + 'bastd.game.conquest.ConquestGame', + ): + entry['type'] = 'bascenev1lib.game.conquest.ConquestGame' + if entry['type'] in ( + 'Elimination.EliminationGame', + 'bsElimination.EliminationGame', + 'bs_elimination.EliminationGame', + 'bastd.game.elimination.EliminationGame', + ): + entry['type'] = 'bascenev1lib.game.elimination.EliminationGame' + if entry['type'] in ( + 'Football.FootballGame', + 'bsFootball.FootballTeamGame', + 'bs_football.FootballTeamGame', + 'bastd.game.football.FootballTeamGame', + ): + entry['type'] = 'bascenev1lib.game.football.FootballTeamGame' + if entry['type'] in ( + 'Hockey.HockeyGame', + 'bsHockey.HockeyGame', + 'bs_hockey.HockeyGame', + 'bastd.game.hockey.HockeyGame', + ): + entry['type'] = 'bascenev1lib.game.hockey.HockeyGame' + if entry['type'] in ( + 'Keep_Away.KeepAwayGame', + 'bsKeepAway.KeepAwayGame', + 'bs_keep_away.KeepAwayGame', + 'bastd.game.keepaway.KeepAwayGame', + ): + entry['type'] = 'bascenev1lib.game.keepaway.KeepAwayGame' + if entry['type'] in ( + 'Race.RaceGame', + 'bsRace.RaceGame', + 'bs_race.RaceGame', + 'bastd.game.race.RaceGame', + ): + entry['type'] = 'bascenev1lib.game.race.RaceGame' + if entry['type'] in ( + 'bsEasterEggHunt.EasterEggHuntGame', + 'bs_easter_egg_hunt.EasterEggHuntGame', + 'bastd.game.easteregghunt.EasterEggHuntGame', + ): + entry['type'] = ( + 'bascenev1lib.game.easteregghunt.EasterEggHuntGame' + ) + if entry['type'] in ( + 'bsMeteorShower.MeteorShowerGame', + 'bs_meteor_shower.MeteorShowerGame', + 'bastd.game.meteorshower.MeteorShowerGame', + ): + entry['type'] = ( + 'bascenev1lib.game.meteorshower.MeteorShowerGame' + ) + if entry['type'] in ( + 'bsTargetPractice.TargetPracticeGame', + 'bs_target_practice.TargetPracticeGame', + 'bastd.game.targetpractice.TargetPracticeGame', + ): + entry['type'] = ( + 'bascenev1lib.game.targetpractice.TargetPracticeGame' + ) + except Exception: + if print_exc: + logging.exception('Error in new_filter_playlist.') + + neededsettings = list[bs.Setting]() + gameclass = None + try: + gameclass = babase.getclass(entry['type'], GameActivity) + + if remove_unowned and gameclass in unowned_game_types: + continue + if add_resolved_type: + entry['resolved_type'] = gameclass + if mark_unowned and gameclass in unowned_game_types: + entry['is_unowned_game'] = True + neededsettings = gameclass.get_available_settings(sessiontype) + + except babase.MapNotFoundError: + if print_exc: + logging.warning( + 'Map \'%s\' not found while scanning playlist \'%s\'.', + entry['settings']['map'], + name, + ) + except ImportError as e: + if print_exc: + logging.warning( + 'Import failed while scanning playlist \'%s\': %s', name, e + ) + entry['is_game_invalid'] = True + # This exception usually happens when we could get the game 'module' + # but, we couldn't get game's GameActivity class name from `entry['type']` + except AttributeError as e: + logging.warning( + 'Get class failed while scanning playlist \'%s\': %s', name, e + ) + entry['is_game_invalid'] = True + + # We 'manually' add some of basic ba*.setting(s) to the filter + # if it exists in raw settings + if entry['settings']['map'] not in available_maps: + entry['is_map_invalid'] = True + + if mark_unowned and entry['settings']['map'] in unowned_maps: + entry['is_unowned_map'] = True + + # Make sure all settings the game defines are present. + for setting in neededsettings: + if setting.name not in entry['settings']: + entry['settings'][setting.name] = setting.default + + goodlist.append(entry) + + return goodlist + + +def apply_packages(): + bauiv1lib.playlist.editcontroller.PlaylistEditController = FluffyPlaylistEditController # Playlist editor controller for PlaylistEditWindow + + bauiv1lib.playlist.editgame.PlaylistEditGameWindow = FluffyPlaylistEditGameWindow # Playlist Game Editer Window + bauiv1lib.playlist.edit.PlaylistEditWindow = FluffyPlaylistEditWindow # Playlist Editor Window + + bs.filter_playlist = new_filter_playlist + bs._playlist.filter_playlist = new_filter_playlist # pyright: ignore[reportAttributeAccessIssue] + + +# ba_meta export babase.Plugin +class by_FluffyPal(Plugin): + def on_app_running(self) -> None: + apply_packages() From 99cf837498a9a9309bd58880f53aec82b874868d Mon Sep 17 00:00:00 2001 From: FluffyPal0 <149132594+FluffyPal0@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:14:50 +0000 Subject: [PATCH 66/84] [ci] apply-plugin-metadata-and-formatting --- plugins/utilities.json | 14 + plugins/utilities/fluffyplaylisteditor.py | 408 +++++++++++----------- 2 files changed, 227 insertions(+), 195 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 7f56f32..a8d34a6 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2588,6 +2588,20 @@ "md5sum": "6346cd7146baf41eef8926dced23fdfe" } } + }, + "fluffyplaylisteditor": { + "description": "A simple not-so advanced Playlist Editor", + "external_url": "https://discord.com/channels/1001896771347304639/1483463979450896445", + "authors": [ + { + "name": "FluffyPal", + "email": "", + "discord": "fluffypal" + } + ], + "versions": { + "1.1.0": null + } } } } \ No newline at end of file diff --git a/plugins/utilities/fluffyplaylisteditor.py b/plugins/utilities/fluffyplaylisteditor.py index 1d528f2..6f81fc8 100644 --- a/plugins/utilities/fluffyplaylisteditor.py +++ b/plugins/utilities/fluffyplaylisteditor.py @@ -1,98 +1,14 @@ # ba_meta require api 9 -plugman = dict( - plugin_name="fluffyplaylisteditor", - description="A simple not-so advanced Playlist Editor", - external_url="https://discord.com/channels/1001896771347304639/1483463979450896445", - authors=[ - {"name": "FluffyPal", "email": "", "discord": "fluffypal"} - ], - version="1.1.0", -) - -### Started: 12 Mar 2026 at night from scratch -# - Learning workflow -# - configuring packages -# - PlaylistEditWindow UI Polishing (I HATE UI POLISHING) - -### Continued: 13 Mar 2026 -# - PlaylistEditGameWindow UI Polishing (AAAA.. oh, found a bug) -# - Implemented GameEdit configs reset and restore -# - Implemented GameEdit map quick navigation buttons -# - Lazy TypedDict PlaylistType - -### Continued: 17 Mar 2026 -# - PlaylistEditController logics overwrite -# - Implemented PlaylistEdit duplicate game -# - Refactor filter_playlist -# - Implemented logics to show invalid games/maps in PlaylistEdit -# - Refine GameEdit reset and restore - -### Continued: 7 June 2026 -# - Fixed map select not recorded on MapSelect -# - Added batch game adding within supported maps - - -import logging -from copy import deepcopy -from random import choice #, randint, randrange - - -import bauiv1 as bui -import bascenev1 as bs - -import babase -from babase import app, Plugin - - -#======= PLAYLIST PARENT =======# -from bascenev1._playlist import filter_playlist #, PlaylistType -from bauiv1lib.playlist import PlaylistTypeVars - -#======= PLAYLIST BORWSER =======# -# Main Playlist Browser Window For Starting The Playlist -# Then goes to PlaylistCustomizeBrowserWindow if pressing "customize..." -#from bauiv1lib.playlist.browser import PlaylistBrowserWindow - -#import bauiv1lib.playlist.customizebrowser -#from bauiv1lib.playlist.customizebrowser import PlaylistCustomizeBrowserWindow - -#======= PLAYLIST ADDING/EDITING =======# -## Main packages to modify -#- Game edit window -import bauiv1lib.playlist.edit -from bauiv1lib.playlist.edit import PlaylistEditWindow - -#from bauiv1lib.playlist.addgame import PlaylistAddGameWindow - -#- Playlist games edit window -import bauiv1lib.playlist.editgame -from bauiv1lib.playlist.editgame import PlaylistEditGameWindow - -# Window for adding registered games to the playlist and then goes to PlaylistEditController for editing -# The UI that shows games GameActivities names on the left and desc on the right -# And "Get More Games..." on the bottom -# After selecting the game, we goes to PlaylistEditGameWindow for configuring the game Settings and Map -import bauiv1lib.playlist.editcontroller -from bauiv1lib.playlist.editcontroller import PlaylistEditController - -#- Game map select window -#import bauiv1lib.playlist.mapselect -#from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow - - -#======= PLAYLIST PLAYING =======# -# This is used when we wanna "play" the playlist -#from bauiv1lib.play import PlayWindow, PlaylistSelectContext - -#from bauiv1lib.playoptions import PlayOptionsWindow - - - -#======= UI Packages =======# -from babase import ( - get_virtual_screen_size +from bascenev1._playlist import filter_playlist # , PlaylistType +from random import choice # , randint, randrange +from bascenev1 import ( + get_filtered_map_name, + get_map_class, + get_map_display_string, ) +from typing import Callable, Sequence, Literal, TypedDict, Any, cast +from bauiv1lib.confirm import ConfirmWindow from bauiv1 import ( # Window utils Window, MainWindowState, @@ -113,11 +29,94 @@ from bauiv1 import ( gettexture, Mesh, Texture, Lstr, ) -from bauiv1lib.confirm import ConfirmWindow +from babase import ( + get_virtual_screen_size +) +from bauiv1lib.playlist.editcontroller import PlaylistEditController +import bauiv1lib.playlist.editcontroller +from bauiv1lib.playlist.editgame import PlaylistEditGameWindow +import bauiv1lib.playlist.editgame +from bauiv1lib.playlist.edit import PlaylistEditWindow +import bauiv1lib.playlist.edit +from bauiv1lib.playlist import PlaylistTypeVars +from babase import app, Plugin +import babase +import bascenev1 as bs +import bauiv1 as bui +from copy import deepcopy +import logging +plugman = dict( + plugin_name="fluffyplaylisteditor", + description="A simple not-so advanced Playlist Editor", + external_url="https://discord.com/channels/1001896771347304639/1483463979450896445", + authors=[ + {"name": "FluffyPal", "email": "", "discord": "fluffypal"} + ], + version="1.1.0", +) -#======= Type Hints =======# -#from enum import Enum -from typing import Callable, Sequence, Literal, TypedDict, Any, cast +# Started: 12 Mar 2026 at night from scratch +# - Learning workflow +# - configuring packages +# - PlaylistEditWindow UI Polishing (I HATE UI POLISHING) + +# Continued: 13 Mar 2026 +# - PlaylistEditGameWindow UI Polishing (AAAA.. oh, found a bug) +# - Implemented GameEdit configs reset and restore +# - Implemented GameEdit map quick navigation buttons +# - Lazy TypedDict PlaylistType + +# Continued: 17 Mar 2026 +# - PlaylistEditController logics overwrite +# - Implemented PlaylistEdit duplicate game +# - Refactor filter_playlist +# - Implemented logics to show invalid games/maps in PlaylistEdit +# - Refine GameEdit reset and restore + +# Continued: 7 June 2026 +# - Fixed map select not recorded on MapSelect +# - Added batch game adding within supported maps + + +# ======= PLAYLIST PARENT =======# + +# ======= PLAYLIST BORWSER =======# +# Main Playlist Browser Window For Starting The Playlist +# Then goes to PlaylistCustomizeBrowserWindow if pressing "customize..." +# from bauiv1lib.playlist.browser import PlaylistBrowserWindow + +# import bauiv1lib.playlist.customizebrowser +# from bauiv1lib.playlist.customizebrowser import PlaylistCustomizeBrowserWindow + +# ======= PLAYLIST ADDING/EDITING =======# +# Main packages to modify +# - Game edit window + +# from bauiv1lib.playlist.addgame import PlaylistAddGameWindow + +# - Playlist games edit window + +# Window for adding registered games to the playlist and then goes to PlaylistEditController for editing +# The UI that shows games GameActivities names on the left and desc on the right +# And "Get More Games..." on the bottom +# After selecting the game, we goes to PlaylistEditGameWindow for configuring the game Settings and Map + +# - Game map select window +# import bauiv1lib.playlist.mapselect +# from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow + + +# ======= PLAYLIST PLAYING =======# +# This is used when we wanna "play" the playlist +# from bauiv1lib.play import PlayWindow, PlaylistSelectContext + +# from bauiv1lib.playoptions import PlayOptionsWindow + + +# ======= UI Packages =======# + +# ======= Type Hints =======# +# from enum import Enum class UntotalOldTypedPlaylistDict(TypedDict, total=False): @@ -129,17 +128,22 @@ class UntotalOldTypedPlaylistDict(TypedDict, total=False): is_map_invalid: bool is_game_invalid: bool + class OldTypedPlaylistDict(TypedDict): map: str level: str + class TypedPlaylistDict(UntotalOldTypedPlaylistDict): settings: OldTypedPlaylistDict type: str + TypedPlaylistType = list[TypedPlaylistDict] -#-------------------------- UI TOOLS --------------------------# +# -------------------------- UI TOOLS --------------------------# + + def buttonwidget( *, edit: Widget | None = None, @@ -154,7 +158,7 @@ def buttonwidget( up_widget: Widget | None = None, left_widget: Widget | None = None, right_widget: Widget | None = None, - #texture: Texture | None = None, + # texture: Texture | None = None, text_scale: float | None = None, textcolor: Sequence[float] | None = None, enable_sound: bool | None = None, @@ -226,13 +230,14 @@ def buttonwidget( better_bg_fit=better_bg_fit ) + def create_container(size: tuple[float, float], **kwargs) -> Widget: """Create the main window with overlay.""" # Container p = containerwidget( - #parent=get_special_widget('overlay_stack'), + # parent=get_special_widget('overlay_stack'), background=False, - #transition='in_scale', + # transition='in_scale', size=size, **kwargs ) @@ -257,7 +262,7 @@ def create_container(size: tuple[float, float], **kwargs) -> Widget: tilt_scale=6, color=tuple(col*1.5 for col in color_theme.get_color('primary')) ) - f = imagewidget( # Foreground + f = imagewidget( # Foreground texture=gettexture('white'), parent=p, size=size, @@ -272,35 +277,46 @@ def create_container(size: tuple[float, float], **kwargs) -> Widget: return p + def create_confirm(callback: Callable, text: str) -> Callable: return lambda: ConfirmWindow( text=f"{text}?", - color=color_theme.get_color('primary'), # pyright: ignore[reportArgumentType] + color=color_theme.get_color('primary'), # pyright: ignore[reportArgumentType] action=callback ) + _SOUND_VOL = 1.2 + + def play_error_sound(): bui.getsound('error').play(_SOUND_VOL) + def play_guncocking_sound(): bui.getsound('gunCocking').play(_SOUND_VOL) + def play_ding_sound(): bui.getsound('ding').play(_SOUND_VOL) + def play_powerdown_sound(): bui.getsound('powerdown01').play(_SOUND_VOL) + def play_shield_down_sound(): bui.getsound('shieldDown').play(_SOUND_VOL) + def play_shield_up_sound(): bui.getsound('shieldUp').play(_SOUND_VOL) + def play_deek_sound(): bui.getsound(choice(['deek', 'deek2'])).play(_SOUND_VOL) + def play_click_sound(): bui.getsound('click01').play(_SOUND_VOL) @@ -324,6 +340,8 @@ class ScreenmessageColors: _COLOR_TYPE = Literal[ 'bg', 'primary', 'secondary', 'tertiary', 'unknown' ] + + class ColorTheme: main_color: Sequence[float] = (0.8, 0.8, 0.8) colors: dict[_COLOR_TYPE, Sequence[float]] = { @@ -341,7 +359,7 @@ class ColorTheme: color_theme = ColorTheme() -#-------------------------- MAIN --------------------------# +# -------------------------- MAIN --------------------------# class FluffyPlaylistEditController(PlaylistEditController): def __init__( self, @@ -386,7 +404,7 @@ class FluffyPlaylistEditController(PlaylistEditController): ], sessiontype=sessiontype, remove_unowned=False, - #add_resolved_type=True, # HACK: This would cause json error as we save a Python class which aren't serializable + # add_resolved_type=True, # HACK: This would cause json error as we save a Python class which aren't serializable name=existing_playlist_name, print_exc=False ) @@ -426,7 +444,6 @@ class FluffyPlaylistEditController(PlaylistEditController): # skip back to there once we're fully done. self._back_state = editwindow.main_window_back_state - def duplicate_game_pressed(self) -> bool: """Duplicate the currently selected game in the playlist and insert it just after.""" if not self._playlist or self._selected_index is None: @@ -459,7 +476,8 @@ class FluffyPlaylistEditController(PlaylistEditController): entry = self._playlist[self._selected_index] if (epic_setting := 'Epic Mode') in entry['settings'] and isinstance(entry['settings'][epic_setting], bool): - self._playlist[self._selected_index]['settings'][epic_setting] = not entry['settings'][epic_setting] # pyright: ignore[reportGeneralTypeIssues] + # pyright: ignore[reportGeneralTypeIssues] + self._playlist[self._selected_index]['settings'][epic_setting] = not entry['settings'][epic_setting] else: play_error_sound() return False @@ -473,7 +491,8 @@ class FluffyPlaylistEditController(PlaylistEditController): entry = self._playlist[self._selected_index] if (solo_setting := 'Solo Mode') in entry['settings'] and isinstance(entry['settings'][solo_setting], bool): - self._playlist[self._selected_index]['settings'][solo_setting] = not entry['settings'][solo_setting] # pyright: ignore[reportGeneralTypeIssues] + # pyright: ignore[reportGeneralTypeIssues] + self._playlist[self._selected_index]['settings'][solo_setting] = not entry['settings'][solo_setting] else: play_error_sound() return False @@ -531,7 +550,8 @@ class FluffyPlaylistEditController(PlaylistEditController): subclassof=bs.GameActivity ) except AttributeError as e: - bs.screenmessage(f'Can\'t edit game: {e}. Maybe try fix it?', ScreenmessageColors.YELLOW) + bs.screenmessage( + f'Can\'t edit game: {e}. Maybe try fix it?', ScreenmessageColors.YELLOW) play_error_sound() return @@ -543,11 +563,11 @@ class FluffyPlaylistEditController(PlaylistEditController): self._show_edit_ui( gametype=cls, - settings=playlist['settings'], # pyright: ignore[reportArgumentType] + settings=playlist['settings'], # pyright: ignore[reportArgumentType] from_window=from_window, ) - def _show_edit_ui( # pyright: ignore[reportIncompatibleMethodOverride] + def _show_edit_ui( # pyright: ignore[reportIncompatibleMethodOverride] self, gametype: type[bs.GameActivity], settings: TypedPlaylistDict | None, @@ -577,7 +597,7 @@ class FluffyPlaylistEditController(PlaylistEditController): assert self._pre_game_edit_state is None self._pre_game_edit_state = editwindow.main_window_back_state - def _edit_game_done( # pyright: ignore[reportIncompatibleMethodOverride] + def _edit_game_done( # pyright: ignore[reportIncompatibleMethodOverride] self, config: TypedPlaylistDict | None, from_window: bui.MainWindow ) -> None: """Called after finished editing/adding a game""" @@ -616,7 +636,6 @@ class FluffyPlaylistEditController(PlaylistEditController): self._playlist.insert(insert_index, new_config) insert_index += 1 - play_guncocking_sound() # If we're adding, jump to before the add started. @@ -635,7 +654,6 @@ class FluffyPlaylistEditController(PlaylistEditController): self._pre_game_add_state = None self._is_batch_add = False - def _get_supported_maps_for_game(self, config: TypedPlaylistDict, gametype: type[bs.GameActivity]): assert app.classic is not None store = app.classic.store @@ -650,7 +668,6 @@ class FluffyPlaylistEditController(PlaylistEditController): return sorted(valid_maps) - def batch_add_game_pressed(self, from_window: MainWindow): self._is_batch_add = True self.add_game_pressed(from_window) @@ -665,12 +682,12 @@ class FluffyPlaylistEditWindow(PlaylistEditWindow): ): self._list_widgets: list[Widget] = [] self._editcontroller = editcontroller - editcontroller._is_batch_add = False # HACK: Couldn't think of any clean idea :p + editcontroller._is_batch_add = False # HACK: Couldn't think of any clean idea :p self._r = 'editGameListWindow' prev_selection: str | None = self._editcontroller.get_edit_ui_selection() - ### Container Setup + # Container Setup uiscale = app.ui_v1.uiscale is_small_ui = uiscale is UIScale.SMALL # Keep a constant aspect ratio for all UIScale values. @@ -690,25 +707,25 @@ class FluffyPlaylistEditWindow(PlaylistEditWindow): self._width = int(self._width) self._height = int(self._height) - MainWindow.__init__(self, # pyright: ignore[reportArgumentType] - root_widget=create_container( - size=(self._width, self._height), - scale=( - 1.20 if is_small_ui else - 0.80 if uiscale is UIScale.MEDIUM else - 0.60 - ), - toolbar_visibility=( - 'menu_minimal_no_back' - if is_small_ui - else 'menu_full' - ), - ), - transition=transition, - origin_widget=origin_widget, - ) + MainWindow.__init__(self, # pyright: ignore[reportArgumentType] + root_widget=create_container( + size=(self._width, self._height), + scale=( + 1.20 if is_small_ui else + 0.80 if uiscale is UIScale.MEDIUM else + 0.60 + ), + toolbar_visibility=( + 'menu_minimal_no_back' + if is_small_ui + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, + ) - ### Widgets/children + # Widgets/children # Buttons self.b_color = b_color = color_theme.get_color('primary') self.b_textcolor = b_textcolor = color_theme.get_color('secondary') @@ -1028,8 +1045,8 @@ class FluffyPlaylistEditWindow(PlaylistEditWindow): subs=[ ('${NAME}', name), ('${MAP}', bs.get_map_display_string( - bs.get_filtered_map_name(pentry['settings']['map'])), - ), + bs.get_filtered_map_name(pentry['settings']['map'])), + ), ], ) elif 'map' in pentry: @@ -1038,8 +1055,8 @@ class FluffyPlaylistEditWindow(PlaylistEditWindow): subs=[ ('${NAME}', name), ('${MAP}', bs.get_map_display_string( - bs.get_filtered_map_name(pentry['map'])), - ), + bs.get_filtered_map_name(pentry['map'])), + ), ], ) else: @@ -1048,13 +1065,11 @@ class FluffyPlaylistEditWindow(PlaylistEditWindow): return sval - def _duplicate_selected_game(self) -> None: if self._editcontroller.duplicate_game_pressed(): play_guncocking_sound() self._refresh() - def _move_down(self) -> None: if len(self._editcontroller.get_playlist()) > 1: super()._move_down() @@ -1063,19 +1078,16 @@ class FluffyPlaylistEditWindow(PlaylistEditWindow): if len(self._editcontroller.get_playlist()) > 1: super()._move_up() - def _toggle_epic_mode(self): if self._editcontroller.toggle_epic_mode(): play_guncocking_sound() self._refresh() - def _toggle_solo_mode(self): if self._editcontroller.toggle_solo_mode(): play_guncocking_sound() self._refresh() - def _refresh(self) -> None: # Need to grab this here as rebuilding the list will # change it otherwise. @@ -1083,15 +1095,17 @@ class FluffyPlaylistEditWindow(PlaylistEditWindow): while self._list_widgets: self._list_widgets.pop().delete() - for index, pentry in enumerate(self._editcontroller.get_playlist()): # pyright: ignore[reportAssignmentType] + # pyright: ignore[reportAssignmentType] + for index, pentry in enumerate(self._editcontroller.get_playlist()): pentry: TypedPlaylistDict try: cls = babase.getclass(pentry['type'], subclassof=bs.GameActivity) - desc = cls.get_settings_display_string(pentry) # pyright: ignore[reportArgumentType] + # pyright: ignore[reportArgumentType] + desc = cls.get_settings_display_string(pentry) color = (0.8, 0.8, 0.8, 1.0) except Exception: - #logging.exception('Error in playlist refresh.') - #desc = "(invalid: '" + pentry['type'] + "')" + # logging.exception('Error in playlist refresh.') + # desc = "(invalid: '" + pentry['type'] + "')" desc = self._get_invalid_game_name(pentry) color = color_theme.get_color('unknown') @@ -1129,11 +1143,6 @@ class FluffyPlaylistEditWindow(PlaylistEditWindow): self._editcontroller.batch_add_game_pressed(from_window=self) -from bascenev1 import ( - get_filtered_map_name, - get_map_class, - get_map_display_string, -) class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): def __init__( @@ -1205,7 +1214,7 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): filtered_map_name = get_filtered_map_name(raw_map) if filtered_map_name not in unowned_maps: self._map = filtered_map_name - #else: + # else: # raise Exception() except Exception: logging.exception('Error getting map for editor.') @@ -1224,7 +1233,7 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): else: self._settings = config else: - self._settings: OldTypedPlaylistDict = {} # pyright: ignore[reportAttributeAccessIssue] + self._settings: OldTypedPlaylistDict = {} # pyright: ignore[reportAttributeAccessIssue] self._settings['map'] = self._map self._default_settings = deepcopy(self._settings) @@ -1238,7 +1247,7 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): raise RuntimeError(f'No map preview tex found for {self._map}.') self._choice_selections: dict[str, int] = {} - ### Container Setup + # Container Setup uiscale = app.ui_v1.uiscale is_small_ui = uiscale is UIScale.SMALL # Keep a constant aspect ratio for all UIScale values. @@ -1249,32 +1258,32 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): self._width = width = int(base_width * 1.65) self._height = height = int(base_width / aspect_ratio) - y_extra2 = 50 # For topper widget elements + y_extra2 = 50 # For topper widget elements # Adjust x_inset proportionally to width self.x_inset = int(width * (0.09 if is_small_ui else 0.0225)) self.yoffs = yoffs = -68 if is_small_ui else -30 MainWindow.__init__(self, - root_widget=containerwidget( - size=(width, height), - color=color_theme.get_color('bg'), - scale=( - 1.10 if is_small_ui else - 0.80 if uiscale is UIScale.MEDIUM else - 0.70 - ), - toolbar_visibility=( - 'menu_minimal_no_back' - if uiscale is UIScale.SMALL - else 'menu_full' - ), - ), - transition=transition, - origin_widget=origin_widget, - ) + root_widget=containerwidget( + size=(width, height), + color=color_theme.get_color('bg'), + scale=( + 1.10 if is_small_ui else + 0.80 if uiscale is UIScale.MEDIUM else + 0.70 + ), + toolbar_visibility=( + 'menu_minimal_no_back' + if uiscale is UIScale.SMALL + else 'menu_full' + ), + ), + transition=transition, + origin_widget=origin_widget, + ) - ### Widgets/children + # Widgets/children b_color = color_theme.get_color('primary') b_textcolor = color_theme.get_color('secondary') @@ -1312,7 +1321,8 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): self.add_button = add_button = original_buttonwidget( parent=self._root_widget, - position=(width - ((255 if is_small_ui else 235) + self.x_inset), height - 82 + y_extra2 + yoffs), + position=(width - ((255 if is_small_ui else 235) + self.x_inset), + height - 82 + y_extra2 + yoffs), size=(200, 65), scale=0.75, text_scale=1.3, @@ -1338,7 +1348,8 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): color=b_color, textcolor=b_textcolor, label=reset_config_text, - on_activate_call=create_confirm(self._reset_settings, reset_config_text.replace('\n', ' ')), + on_activate_call=create_confirm( + self._reset_settings, reset_config_text.replace('\n', ' ')), icon=gettexture('replayIcon'), iconscale=1.5 ) @@ -1355,7 +1366,8 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): textcolor=b_textcolor, label=restore_config_text, on_activate_call=( - create_confirm(self._restore_settings, restore_config_text.replace('\n', ' ')) if not is_add else play_error_sound + create_confirm(self._restore_settings, restore_config_text.replace( + '\n', ' ')) if not is_add else play_error_sound ), icon=gettexture('leftButton'), iconscale=1.5 @@ -1408,14 +1420,14 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): # Only update keys which are different, and track if anything actually changed for key, value in self._default_settings.items(): if key not in self._settings or self._settings[key] != value: - #print(f'key restored: {self._settings[key]} -> {key}') + # print(f'key restored: {self._settings[key]} -> {key}') self._settings[key] = value updated = True # Check if map changed, update relevant attrs restored_map = get_filtered_map_name(self._settings['map']) if self._map != restored_map: - #print(f'map restored: {self._map} -> {restored_map}') + # print(f'map restored: {self._map} -> {restored_map}') self._map = restored_map try: self.map_tex_name = get_map_class(self._map).get_preview_texture_name() @@ -1457,7 +1469,8 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): """Shift selection in the valid maps list by target (wrap around)""" valid_maps = self._valid_maps_owned - cur_map = self._map; assert cur_map + cur_map = self._map + assert cur_map try: cur_map_index = valid_maps.index(cur_map) except ValueError: @@ -1481,11 +1494,11 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): scroll_width = self._width - (86 + (3 if is_small_ui else 5.5) * self.x_inset) - spacing = 47 # Scroller playlist config items spacing - y_extra = 15 # For scroller widget + spacing = 47 # Scroller playlist config items spacing + y_extra = 15 # For scroller widget # Calc our total height we'll need - scroll_height = map_height + 10 # map select and margin + scroll_height = map_height + 10 # map select and margin scroll_height += spacing * len(self._settings_defs) if not self._scrollwidget: @@ -1552,7 +1565,7 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): self._map_buttonwidget = None if len(self._valid_maps_owned) > 1: - original_buttonwidget( # map_prev + original_buttonwidget( # map_prev parent=self._subcontainer, position=(h + scroll_width * 0.46 - 50 - 1, v - 63), size=(35, 35), @@ -1564,7 +1577,7 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): enable_sound=False, repeat=True, ) - original_buttonwidget( # map_next + original_buttonwidget( # map_next parent=self._subcontainer, position=(h + scroll_width * (0.655 if is_small_ui else 0.6225) + 5, v - 63), size=(35, 35), @@ -1616,7 +1629,8 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): ) v -= map_height - config = self._settings; assert config + config = self._settings + assert config for setting in self._settings_defs: value = setting.default value_type = type(value) @@ -1660,12 +1674,12 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): ) if not isinstance(choice[1], value_type): invalid = True - #raise TypeError( + # raise TypeError( # 'Choice type does not match default value; choice:' # + repr(choice) # + '; setting:' # + repr(setting) - #) + # ) if value_type not in (int, float): raise TypeError( 'Choice type setting must have int or float default; ' @@ -1867,6 +1881,8 @@ class FluffyPlaylistEditGameWindow(PlaylistEditGameWindow): original_filter_playlist = filter_playlist + + def new_filter_playlist( playlist: TypedPlaylistDict, sessiontype: type[bs.Session], @@ -1899,7 +1915,7 @@ def new_filter_playlist( unowned_maps = [] unowned_game_types = set() - for entry in deepcopy(playlist): # pyright: ignore[reportAssignmentType] + for entry in deepcopy(playlist): # pyright: ignore[reportAssignmentType] entry: TypedPlaylistDict # 'map' used to be called 'level' here. if 'level' in entry: @@ -2066,8 +2082,8 @@ def new_filter_playlist( # but, we couldn't get game's GameActivity class name from `entry['type']` except AttributeError as e: logging.warning( - 'Get class failed while scanning playlist \'%s\': %s', name, e - ) + 'Get class failed while scanning playlist \'%s\': %s', name, e + ) entry['is_game_invalid'] = True # We 'manually' add some of basic ba*.setting(s) to the filter @@ -2089,13 +2105,15 @@ def new_filter_playlist( def apply_packages(): - bauiv1lib.playlist.editcontroller.PlaylistEditController = FluffyPlaylistEditController # Playlist editor controller for PlaylistEditWindow + # Playlist editor controller for PlaylistEditWindow + bauiv1lib.playlist.editcontroller.PlaylistEditController = FluffyPlaylistEditController - bauiv1lib.playlist.editgame.PlaylistEditGameWindow = FluffyPlaylistEditGameWindow # Playlist Game Editer Window - bauiv1lib.playlist.edit.PlaylistEditWindow = FluffyPlaylistEditWindow # Playlist Editor Window + bauiv1lib.playlist.editgame.PlaylistEditGameWindow = FluffyPlaylistEditGameWindow # Playlist Game Editer Window + bauiv1lib.playlist.edit.PlaylistEditWindow = FluffyPlaylistEditWindow # Playlist Editor Window bs.filter_playlist = new_filter_playlist - bs._playlist.filter_playlist = new_filter_playlist # pyright: ignore[reportAttributeAccessIssue] + # pyright: ignore[reportAttributeAccessIssue] + bs._playlist.filter_playlist = new_filter_playlist # ba_meta export babase.Plugin From 7e249c1181ed42f2b6324e226c228bdf6a1c27de Mon Sep 17 00:00:00 2001 From: FluffyPal0 <149132594+FluffyPal0@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:14:51 +0000 Subject: [PATCH 67/84] [ci] apply-version-metadata --- plugins/utilities.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index a8d34a6..febc33b 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2600,7 +2600,12 @@ } ], "versions": { - "1.1.0": null + "1.1.0": { + "api_version": 9, + "commit_sha": "99cf837", + "released_on": "08-06-2026", + "md5sum": "3ead338c768fbdffe022aadcbdc8c1d8" + } } } } From d88d0e149dd258af807446cfbd2abf0548c21d43 Mon Sep 17 00:00:00 2001 From: GeoHnlol <107043540+GeoHnlol@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:27:28 +0300 Subject: [PATCH 68/84] Updated export shortcuts Replaced all instances of EXPORT_CLASS_NAME_SHORTCUTS with _DEPRECATED_EXPORT_SHORTCUTS for bs version 1.7.63 --- plugin_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin_manager.py b/plugin_manager.py index fce6017..645df56 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -2,7 +2,7 @@ import babase import bauiv1 as bui from bauiv1lib import popup, confirm -from babase._meta import EXPORT_CLASS_NAME_SHORTCUTS +from babase._meta import _DEPRECATED_EXPORT_SHORTCUTS from bauiv1lib.settings.allsettings import AllSettingsWindow import urllib.request @@ -82,7 +82,7 @@ REGEXP = { "plugin_entry_points": re.compile( bytes( "(ba_meta export (plugin|{})\n+class )(.*)\\(".format( - _regexp_friendly_class_name_shortcut(EXPORT_CLASS_NAME_SHORTCUTS["plugin"]), + _regexp_friendly_class_name_shortcut(_DEPRECATED_EXPORT_SHORTCUTS["plugin"]), ), "utf-8" ), From e091fb6983613abbbe24b62babd7bf8bd11c7efe Mon Sep 17 00:00:00 2001 From: "Mr.Paradox" Date: Wed, 10 Jun 2026 09:18:14 +0530 Subject: [PATCH 69/84] Update and rename vanishing_tiles.py to vanishing_tiles.py --- .../vanishing_tiles.py | 236 +++++++----------- 1 file changed, 88 insertions(+), 148 deletions(-) rename plugins/{utilities => minigames}/vanishing_tiles.py (64%) diff --git a/plugins/utilities/vanishing_tiles.py b/plugins/minigames/vanishing_tiles.py similarity index 64% rename from plugins/utilities/vanishing_tiles.py rename to plugins/minigames/vanishing_tiles.py index 898bf69..78826d0 100644 --- a/plugins/utilities/vanishing_tiles.py +++ b/plugins/minigames/vanishing_tiles.py @@ -1,10 +1,5 @@ # ba_meta require api 9 from __future__ import annotations -from bascenev1lib.gameutils import SharedObjects -import bascenev1 as bs -import babase -import random -from typing import Optional, List, Dict, Any # Vanishing Tiles - BombSquad Minigame # Tiles disappear one by one. Survive all rounds to win. @@ -15,12 +10,18 @@ plugman = dict( description="Tiles disappear gradually. Survive each round to continue. Last player standing wins!", external_url="https://discord.gg/sQGDsztQcy", authors=[ - {"name": "Mr.ghosty", "email": "", "discord": "gaurangbroyo"}, + {"name": "Mr.Paradox", "email": "microsoftusergame@gmail.com", "discord": "gaurangbroyo"}, {"name": "senchx", "email": "", "discord": "senchx0"}, ], - version="2.0.0", + version="1.0.2", ) +from typing import Optional, List, Dict, Any +import random +import babase +import bascenev1 as bs +from bascenev1lib.gameutils import SharedObjects + class Player(bs.Player['Team']): def __init__(self) -> None: @@ -35,7 +36,7 @@ class Team(bs.Team[Player]): # ba_meta export bascenev1.GameActivity class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): - name = 'Vanishing Tiles' + name = 'Vanishing Tiles' description = 'Tiles disappear one by one. Be the last one standing!' scoreconfig = bs.ScoreConfig(label='Survived', scoretype=bs.ScoreType.MILLISECONDS) announce_player_deaths = True @@ -57,19 +58,19 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def __init__(self, settings: dict) -> None: super().__init__(settings) - self._epic_mode: bool = bool(settings.get('Epic Mode', False)) - self._show_credits: bool = bool(settings.get('Show Credits', True)) - self.default_music = bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL + self._epic_mode = bool(settings.get('Epic Mode', False)) + self._show_credits = bool(settings.get('Show Credits', True)) + self.default_music = bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL - self._round: int = 1 - self._removing: bool = False + self._round = 1 + self._removing = False self._game_start_time: Optional[float] = None self._tile_nodes: Dict[int, bs.Node] = {} self._region_nodes: Dict[int, bs.Node] = {} - self._present_tile_ids: set[int] = set() + self._present_tile_ids: set[int] = set() - self._remove_speed: float = 1.8 + self._remove_speed = 1.8 self._collide_mat = bs.Material() self._collide_mat.add_actions(actions=(('modify_part_collision', 'collide', True),)) @@ -77,8 +78,8 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._no_collide_mat.add_actions(actions=(('modify_part_collision', 'collide', False),)) self._default_tex = bs.gettexture('powerupHealth') - self._warn_tex = bs.gettexture('powerupCurse') - self._final_tex = bs.gettexture('powerupPunch') + self._warn_tex = bs.gettexture('powerupCurse') + self._final_tex = bs.gettexture('powerupPunch') self._hud_round: Optional[bs.Node] = None self._hud_players: Optional[bs.Node] = None @@ -90,12 +91,10 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def spawn_player(self, player: Player) -> Any: if isinstance(self.session, bs.FreeForAllSession): - pos = VanishingTilesMapDefs.points['spawn1'] - spaz = self.spawn_player_spaz(player, position=pos) + spaz = self.spawn_player_spaz(player, position=VanishingTilesMapDefs.points['spawn1']) else: spaz = self.spawn_player_spaz(player) - spaz.connect_controls_to_player( - enable_punch=False, enable_pickup=False, enable_bomb=False) + spaz.connect_controls_to_player(enable_punch=False, enable_pickup=False, enable_bomb=False) spaz.set_bomb_count(0) return spaz @@ -105,20 +104,13 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): if self._show_credits: self._credit_node = bs.newnode('text', attrs={ - 'text': 'Made by Mr.Ghosty', - 'scale': 0.7, - 'position': (0, 8), - 'shadow': 0.8, - 'flatness': 1.0, - 'color': (1.0, 0.3, 0.8, 1.0), - 'h_align': 'center', - 'v_attach': 'bottom', + 'text': 'Made by Mr.Paradox & senchx', + 'scale': 0.7, 'position': (0, 8), 'shadow': 0.8, 'flatness': 1.0, + 'color': (1.0, 0.3, 0.8, 1.0), 'h_align': 'center', 'v_attach': 'bottom', }) bs.animate_array(self._credit_node, 'color', 4, { - 0.0: [1.0, 0.3, 0.8, 1.0], - 1.0: [0.3, 0.8, 1.0, 1.0], - 2.0: [0.4, 1.0, 0.4, 1.0], - 3.0: [1.0, 1.0, 0.2, 1.0], + 0.0: [1.0, 0.3, 0.8, 1.0], 1.0: [0.3, 0.8, 1.0, 1.0], + 2.0: [0.4, 1.0, 0.4, 1.0], 3.0: [1.0, 1.0, 0.2, 1.0], 4.0: [1.0, 0.3, 0.8, 1.0], }, loop=True) @@ -129,8 +121,8 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): if not isinstance(self.session, bs.FreeForAllSession): if not all(len(t.players) >= 1 for t in self.teams): - bs.broadcastmessage('Not enough players — draw!', color=(1, 1, 0)) - bs.timer(1.0, bs.CallStrict(self.end, bs.GameResults())) + bs.broadcastmessage('Not enough players - draw!', color=(1, 1, 0)) + bs.timer(1.0, lambda: self.end(bs.GameResults())) return bs.timer(1.0, self._check_initial_state) @@ -139,33 +131,24 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def _build_hud(self) -> None: for attr in ('_hud_round', '_hud_players', '_hud_tiles'): node = getattr(self, attr, None) - if node: - try: - node.delete() - except Exception: - pass + if node is not None and node.exists(): + node.delete() setattr(self, attr, None) self._hud_round = bs.newnode('text', attrs={ - 'text': '', 'scale': 0.85, 'position': (0, -40), - 'maxwidth': 300, 'h_align': 'center', 'v_align': 'center', - 'v_attach': 'top', 'h_attach': 'center', - 'shadow': 1.0, 'flatness': 1.0, 'color': (1, 1, 1, 1), - 'in_world': False, + 'text': '', 'scale': 0.85, 'position': (0, -40), 'maxwidth': 300, + 'h_align': 'center', 'v_align': 'center', 'v_attach': 'top', 'h_attach': 'center', + 'shadow': 1.0, 'flatness': 1.0, 'color': (1, 1, 1, 1), 'in_world': False, }) self._hud_players = bs.newnode('text', attrs={ - 'text': '', 'scale': 0.8, 'position': (0, -58), - 'maxwidth': 300, 'h_align': 'center', 'v_align': 'center', - 'v_attach': 'top', 'h_attach': 'center', - 'shadow': 1.0, 'flatness': 1.0, 'color': (1, 0.8, 0.2, 1), - 'in_world': False, + 'text': '', 'scale': 0.8, 'position': (0, -58), 'maxwidth': 300, + 'h_align': 'center', 'v_align': 'center', 'v_attach': 'top', 'h_attach': 'center', + 'shadow': 1.0, 'flatness': 1.0, 'color': (1, 0.8, 0.2, 1), 'in_world': False, }) self._hud_tiles = bs.newnode('text', attrs={ - 'text': '', 'scale': 0.8, 'position': (0, -74), - 'maxwidth': 300, 'h_align': 'center', 'v_align': 'center', - 'v_attach': 'top', 'h_attach': 'center', - 'shadow': 1.0, 'flatness': 1.0, 'color': (0.5, 1.0, 0.5, 1), - 'in_world': False, + 'text': '', 'scale': 0.8, 'position': (0, -74), 'maxwidth': 300, + 'h_align': 'center', 'v_align': 'center', 'v_attach': 'top', 'h_attach': 'center', + 'shadow': 1.0, 'flatness': 1.0, 'color': (0.5, 1.0, 0.5, 1), 'in_world': False, }) self._refresh_hud() @@ -176,37 +159,27 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): if self._hud_round: self._hud_round.text = f'Round {self._round}' - if self._hud_players: self._hud_players.text = f'Players alive: {alive}' - if self._hud_tiles: self._hud_tiles.color = (1.0, 0.3, 0.3, 1) if urgent else (0.5, 1.0, 0.5, 1) - self._hud_tiles.text = f'Tiles left: {tiles}' + self._hud_tiles.text = f'Tiles left: {tiles}' def _show_round_banner(self) -> None: node = bs.newnode('text', attrs={ - 'text': f'Round {self._round}', - 'scale': 1.3, - 'position': (0, 60), - 'shadow': 1.2, - 'flatness': 0.7, - 'color': (1, 1, 0, 1), - 'h_align': 'center', - 'v_attach': 'center', - 'in_world': False, + 'text': f'Round {self._round}', 'scale': 1.3, 'position': (0, 60), + 'shadow': 1.2, 'flatness': 0.7, 'color': (1, 1, 0, 1), + 'h_align': 'center', 'v_attach': 'center', 'in_world': False, }) bs.animate(node, 'scale', {0: 0.0, 0.15: 1.3, 2.0: 1.3, 2.5: 0.0}) - bs.timer(2.6, bs.CallStrict(node.delete)) + bs.timer(2.6, node.delete) def on_player_join(self, player: Player) -> None: if self.has_begun(): player.death_time = bs.time() bs.broadcastmessage( - f'{player.getname()} joined! Currently Round {self._round} — ' - f'{len(self._present_tile_ids)} tiles left.', - color=(0.5, 1.0, 1.0), - transient=True, + f'{player.getname()} joined! Round {self._round} - {len(self._present_tile_ids)} tiles left.', + color=(0.5, 1.0, 1.0), transient=True, ) return self.spawn_player(player) @@ -222,27 +195,20 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): (-1.5, 2, -9), (-1.5, 2, -6), (-1.5, 2, -3), (-1.5, 2, 0), (-4.5, 2, -9), (-4.5, 2, -6), (-4.5, 2, -3), (-4.5, 2, 0), ] - model = bs.getmesh('buttonSquareOpaque') + model = bs.getmesh('buttonSquareOpaque') shared = SharedObjects.get() for i, pos in enumerate(positions): tile = bs.newnode('prop', attrs={ - 'body': 'puck', - 'position': pos, - 'mesh': model, - 'mesh_scale': 3.73, - 'body_scale': 3.73, - 'gravity_scale': 0.0, - 'color_texture': self._default_tex, - 'reflection': 'soft', - 'materials': [self._no_collide_mat], + 'body': 'puck', 'position': pos, 'mesh': model, + 'mesh_scale': 3.73, 'body_scale': 3.73, 'gravity_scale': 0.0, + 'color_texture': self._default_tex, 'reflection': 'soft', + 'materials': [self._no_collide_mat], }) region = bs.newnode('region', attrs={ - 'position': pos, - 'scale': (3.5, 0.1, 3.5), - 'type': 'box', + 'position': pos, 'scale': (3.5, 0.1, 3.5), 'type': 'box', 'materials': [self._collide_mat, shared.footing_material], }) - self._tile_nodes[i] = tile + self._tile_nodes[i] = tile self._region_nodes[i] = region self._present_tile_ids.add(i) self._refresh_hud() @@ -255,7 +221,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def _make_final_tile(self) -> None: try: tile_id = list(self._present_tile_ids)[0] - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) if tile and tile.exists(): tile.color_texture = self._final_tex except Exception: @@ -266,7 +232,6 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): for p in self.players: if p.is_alive() and p.actor and p.actor.exists(): p.actor.node.handlemessage(bs.CelebrateMessage(3.0)) - self._refresh_hud() bs.timer(3.0, self._round_end_check) @@ -275,8 +240,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._make_final_tile() return - urgent = len(self._present_tile_ids) <= 4 - if urgent: + if len(self._present_tile_ids) <= 4: bs.getsound('shieldDown').play(0.4) tile_id = random.choice(list(self._present_tile_ids)) @@ -284,7 +248,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): bs.timer(self._remove_speed, self._remove_next_tile) def _remove_tile(self, tile_id: int) -> None: - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) region = self._region_nodes.get(tile_id) if tile is None or not tile.exists(): self._present_tile_ids.discard(tile_id) @@ -294,11 +258,8 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def vanish() -> None: if tile.exists(): - bs.emitfx( - position=tile.position, - count=20, scale=0.9, spread=0.5, - chunk_type='spark', - ) + bs.emitfx(position=tile.position, count=20, scale=0.9, + spread=0.5, chunk_type='spark') tile.delete() if region and region.exists(): region.delete() @@ -311,8 +272,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self.slow_motion = self._epic_mode if not isinstance(self.session, bs.FreeForAllSession): - living_teams = [ - t for t in self.teams if any(p.is_alive() for p in t.players)] + living_teams = [t for t in self.teams if any(p.is_alive() for p in t.players)] if len(living_teams) <= 1: self._removing = False self.end_game() @@ -326,15 +286,12 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._remove_speed = max(0.3, self._remove_speed * 0.75) self._round += 1 survivors = [p for p in self.players if p.is_alive()] - bs.broadcastmessage( - f'Round {self._round}! Speed increasing!', color=(0.5, 1.0, 1.0)) - bs.timer(1.5, bs.CallStrict(self._next_round, survivors)) + bs.broadcastmessage(f'Round {self._round}! Speed increasing!', color=(0.5, 1.0, 1.0)) + bs.timer(1.5, lambda s=survivors: self._next_round(s)) def _next_round(self, survivors: List[Player]) -> None: safety = bs.newnode('region', attrs={ - 'position': (0, 1.5, -5), - 'scale': (20, 0.2, 20), - 'type': 'box', + 'position': (0, 1.5, -5), 'scale': (20, 0.2, 20), 'type': 'box', 'materials': [SharedObjects.get().footing_material], }) self._cleanup_tiles() @@ -344,18 +301,16 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): for p in survivors: if p.actor and p.actor.exists(): - p.actor.handlemessage( - bs.StandMessage(VanishingTilesMapDefs.points['spawn1'])) + p.actor.handlemessage(bs.StandMessage(VanishingTilesMapDefs.points['spawn1'])) - bs.timer(2.5, bs.CallStrict(safety.delete)) + bs.timer(2.5, safety.delete) self._removing = False bs.timer(3.0, self._start_removal) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.PlayerDiedMessage): super().handlemessage(msg) - p = msg.getplayer(Player) - p.death_time = bs.time() + msg.getplayer(Player).death_time = bs.time() self._refresh_hud() bs.timer(0.5, self._check_end_game) return None @@ -372,49 +327,40 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): return for team in self.teams: if len(team.players) == 0: - winner = next((t for t in self.teams if t is not team), None) - bs.broadcastmessage('A team has no players — game over!', - color=(1, 1, 0)) - bs.timer(1.0, bs.CallStrict(self.end_game)) + bs.broadcastmessage('A team has no players - game over!', color=(1, 1, 0)) + bs.timer(1.0, self.end_game) return def _check_end_game(self) -> None: if not self.has_begun(): return if not isinstance(self.session, bs.FreeForAllSession): - if len([t for t in self.teams - if any(p.is_alive() for p in t.players)]) <= 1: + if len([t for t in self.teams if any(p.is_alive() for p in t.players)]) <= 1: self.end_game() else: - if len([p for p in self.players if p.is_alive()]) == 0: + if len([p for p in self.players if p.is_alive()]) <= 1: self.end_game() def end_game(self) -> None: if self.has_ended(): return cur_time = bs.time() - start = self._game_start_time or cur_time - results = bs.GameResults() + start = self._game_start_time or cur_time + results = bs.GameResults() for team in self.teams: longest = 0.0 for p in team.players: - death = p.death_time or (cur_time + 1) + death = p.death_time or (cur_time + 1) longest = max(longest, death - start) results.set_team_score(team, int(longest * 1000)) for attr in ('_hud_round', '_hud_players', '_hud_tiles'): node = getattr(self, attr, None) - if node: - try: - node.delete() - except Exception: - pass + if node is not None and node.exists(): + node.delete() - if self._credit_node: - try: - self._credit_node.delete() - except Exception: - pass + if self._credit_node and self._credit_node.exists(): + self._credit_node.delete() self.end(results=results) @@ -432,7 +378,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): class VanishingTilesMapDefs: points = {'spawn1': (0, 3, -5)} - boxes = { + boxes = { 'area_of_interest_bounds': (0, 4, -5, 0, 0, 0, 16, 8, 16), 'map_bounds': (0, 4, -5, 0, 0, 0, 30, 14, 30), } @@ -448,33 +394,27 @@ class VanishingTilesMap(bs.Map): @classmethod def on_preload(cls) -> Any: - return { - 'bgtex': bs.gettexture('menuBG'), - 'bgmesh': bs.getmesh('thePadBG'), - } + return {'bgtex': bs.gettexture('menuBG'), 'bgmesh': bs.getmesh('thePadBG')} def __init__(self) -> None: super().__init__() self.node = bs.newnode('terrain', attrs={ - 'mesh': self.preloaddata['bgmesh'], - 'lighting': False, - 'background': True, - 'color_texture': self.preloaddata['bgtex'], + 'mesh': self.preloaddata['bgmesh'], 'lighting': False, + 'background': True, 'color_texture': self.preloaddata['bgtex'], }) -bs._map.register_map(VanishingTilesMap) +try: + bs.register_map(VanishingTilesMap) +except Exception: + bs._map.register_map(VanishingTilesMap) # ba_meta export babase.Plugin class Main(babase.Plugin): def __init__(self) -> None: - babase.app.classic.add_coop_practice_level( - bs.Level( - name='Vanishing Tiles', - displayname='Vanishing Tiles', - gametype=VanishingTilesGame, - settings={}, - preview_texture_name='powerupHealth', - ) - ) + babase.app.classic.add_coop_practice_level(bs.Level( + name='Vanishing Tiles', displayname='Vanishing Tiles', + gametype=VanishingTilesGame, settings={}, + preview_texture_name='powerupHealth', + )) From edaed7023581d69360c3c9e103550976a7e63903 Mon Sep 17 00:00:00 2001 From: MrParadox1691 <292215964+MrParadox1691@users.noreply.github.com> Date: Wed, 10 Jun 2026 07:05:08 +0000 Subject: [PATCH 70/84] [ci] apply-plugin-metadata-and-formatting --- plugins/minigames.json | 19 +++++++++++ plugins/minigames/vanishing_tiles.py | 47 ++++++++++++++-------------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index 52bcd96..583f84e 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1688,6 +1688,25 @@ "md5sum": "be998b599896ed0e8dd5f2545fae6173" } } + }, + "vanishing_tiles": { + "description": "Tiles disappear gradually. Survive each round to continue. Last player standing wins!", + "external_url": "https://discord.gg/sQGDsztQcy", + "authors": [ + { + "name": "Mr.Paradox", + "email": "microsoftusergame@gmail.com", + "discord": "gaurangbroyo" + }, + { + "name": "senchx", + "email": "", + "discord": "senchx0" + } + ], + "versions": { + "1.0.2": null + } } } } \ No newline at end of file diff --git a/plugins/minigames/vanishing_tiles.py b/plugins/minigames/vanishing_tiles.py index 78826d0..f606a6a 100644 --- a/plugins/minigames/vanishing_tiles.py +++ b/plugins/minigames/vanishing_tiles.py @@ -1,5 +1,10 @@ # ba_meta require api 9 from __future__ import annotations +from bascenev1lib.gameutils import SharedObjects +import bascenev1 as bs +import babase +import random +from typing import Optional, List, Dict, Any # Vanishing Tiles - BombSquad Minigame # Tiles disappear one by one. Survive all rounds to win. @@ -16,12 +21,6 @@ plugman = dict( version="1.0.2", ) -from typing import Optional, List, Dict, Any -import random -import babase -import bascenev1 as bs -from bascenev1lib.gameutils import SharedObjects - class Player(bs.Player['Team']): def __init__(self) -> None: @@ -36,7 +35,7 @@ class Team(bs.Team[Player]): # ba_meta export bascenev1.GameActivity class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): - name = 'Vanishing Tiles' + name = 'Vanishing Tiles' description = 'Tiles disappear one by one. Be the last one standing!' scoreconfig = bs.ScoreConfig(label='Survived', scoretype=bs.ScoreType.MILLISECONDS) announce_player_deaths = True @@ -58,17 +57,17 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def __init__(self, settings: dict) -> None: super().__init__(settings) - self._epic_mode = bool(settings.get('Epic Mode', False)) - self._show_credits = bool(settings.get('Show Credits', True)) - self.default_music = bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL + self._epic_mode = bool(settings.get('Epic Mode', False)) + self._show_credits = bool(settings.get('Show Credits', True)) + self.default_music = bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL - self._round = 1 - self._removing = False + self._round = 1 + self._removing = False self._game_start_time: Optional[float] = None self._tile_nodes: Dict[int, bs.Node] = {} self._region_nodes: Dict[int, bs.Node] = {} - self._present_tile_ids: set[int] = set() + self._present_tile_ids: set[int] = set() self._remove_speed = 1.8 @@ -78,8 +77,8 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._no_collide_mat.add_actions(actions=(('modify_part_collision', 'collide', False),)) self._default_tex = bs.gettexture('powerupHealth') - self._warn_tex = bs.gettexture('powerupCurse') - self._final_tex = bs.gettexture('powerupPunch') + self._warn_tex = bs.gettexture('powerupCurse') + self._final_tex = bs.gettexture('powerupPunch') self._hud_round: Optional[bs.Node] = None self._hud_players: Optional[bs.Node] = None @@ -163,7 +162,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._hud_players.text = f'Players alive: {alive}' if self._hud_tiles: self._hud_tiles.color = (1.0, 0.3, 0.3, 1) if urgent else (0.5, 1.0, 0.5, 1) - self._hud_tiles.text = f'Tiles left: {tiles}' + self._hud_tiles.text = f'Tiles left: {tiles}' def _show_round_banner(self) -> None: node = bs.newnode('text', attrs={ @@ -195,7 +194,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): (-1.5, 2, -9), (-1.5, 2, -6), (-1.5, 2, -3), (-1.5, 2, 0), (-4.5, 2, -9), (-4.5, 2, -6), (-4.5, 2, -3), (-4.5, 2, 0), ] - model = bs.getmesh('buttonSquareOpaque') + model = bs.getmesh('buttonSquareOpaque') shared = SharedObjects.get() for i, pos in enumerate(positions): tile = bs.newnode('prop', attrs={ @@ -208,7 +207,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): 'position': pos, 'scale': (3.5, 0.1, 3.5), 'type': 'box', 'materials': [self._collide_mat, shared.footing_material], }) - self._tile_nodes[i] = tile + self._tile_nodes[i] = tile self._region_nodes[i] = region self._present_tile_ids.add(i) self._refresh_hud() @@ -221,7 +220,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def _make_final_tile(self) -> None: try: tile_id = list(self._present_tile_ids)[0] - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) if tile and tile.exists(): tile.color_texture = self._final_tex except Exception: @@ -248,7 +247,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): bs.timer(self._remove_speed, self._remove_next_tile) def _remove_tile(self, tile_id: int) -> None: - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) region = self._region_nodes.get(tile_id) if tile is None or not tile.exists(): self._present_tile_ids.discard(tile_id) @@ -345,12 +344,12 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): if self.has_ended(): return cur_time = bs.time() - start = self._game_start_time or cur_time - results = bs.GameResults() + start = self._game_start_time or cur_time + results = bs.GameResults() for team in self.teams: longest = 0.0 for p in team.players: - death = p.death_time or (cur_time + 1) + death = p.death_time or (cur_time + 1) longest = max(longest, death - start) results.set_team_score(team, int(longest * 1000)) @@ -378,7 +377,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): class VanishingTilesMapDefs: points = {'spawn1': (0, 3, -5)} - boxes = { + boxes = { 'area_of_interest_bounds': (0, 4, -5, 0, 0, 0, 16, 8, 16), 'map_bounds': (0, 4, -5, 0, 0, 0, 30, 14, 30), } From 7fcb9b4468309abf87ef3f4e661f5cb2e9d90404 Mon Sep 17 00:00:00 2001 From: MrParadox1691 <292215964+MrParadox1691@users.noreply.github.com> Date: Wed, 10 Jun 2026 07:05:09 +0000 Subject: [PATCH 71/84] [ci] apply-version-metadata --- plugins/minigames.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index 583f84e..efc4c40 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1705,7 +1705,12 @@ } ], "versions": { - "1.0.2": null + "1.0.2": { + "api_version": 9, + "commit_sha": "edaed70", + "released_on": "10-06-2026", + "md5sum": "3711549cfb7a5225bbe385b848f070e0" + } } } } From 2e2ff3656ab316e7aac3c83a41862ad60e5daa58 Mon Sep 17 00:00:00 2001 From: "Mr.Paradox" Date: Wed, 10 Jun 2026 21:21:21 +0530 Subject: [PATCH 72/84] Update utilities.json --- plugins/utilities.json | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 7f56f32..42da57d 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2527,30 +2527,6 @@ } } }, - "vanishing_tiles": { - "description": "Tiles disappear gradually. Survive each round to continue. Last player standing wins!", - "external_url": "https://discord.gg/sQGDsztQcy", - "authors": [ - { - "name": "Mr.ghosty", - "email": "", - "discord": "gaurangbroyo" - }, - { - "name": "senchx", - "email": "", - "discord": "senchx0" - } - ], - "versions": { - "2.0.0": { - "api_version": 9, - "commit_sha": "38f9e35", - "released_on": "09-03-2026", - "md5sum": "f2b1b542cf8cb7b633c5b460966bd26c" - } - } - }, "rank_system": { "description": "ranks system for servers or for local players", "external_url": "", @@ -2590,4 +2566,4 @@ } } } -} \ No newline at end of file +} From 4804a55ca0fbb1747446dd2cab738186ab83e564 Mon Sep 17 00:00:00 2001 From: "Mr.Paradox" Date: Wed, 10 Jun 2026 21:41:40 +0530 Subject: [PATCH 73/84] Update vanishing_tiles.py --- plugins/minigames/vanishing_tiles.py | 58 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/plugins/minigames/vanishing_tiles.py b/plugins/minigames/vanishing_tiles.py index f606a6a..fd191fe 100644 --- a/plugins/minigames/vanishing_tiles.py +++ b/plugins/minigames/vanishing_tiles.py @@ -1,10 +1,5 @@ # ba_meta require api 9 from __future__ import annotations -from bascenev1lib.gameutils import SharedObjects -import bascenev1 as bs -import babase -import random -from typing import Optional, List, Dict, Any # Vanishing Tiles - BombSquad Minigame # Tiles disappear one by one. Survive all rounds to win. @@ -15,12 +10,18 @@ plugman = dict( description="Tiles disappear gradually. Survive each round to continue. Last player standing wins!", external_url="https://discord.gg/sQGDsztQcy", authors=[ - {"name": "Mr.Paradox", "email": "microsoftusergame@gmail.com", "discord": "gaurangbroyo"}, + {"name": "Mr.ghosty", "email": "", "discord": "gaurangbroyo"}, {"name": "senchx", "email": "", "discord": "senchx0"}, ], - version="1.0.2", + version="2.1.0", ) +from typing import Optional, List, Dict, Any +import random +import babase +import bascenev1 as bs +from bascenev1lib.gameutils import SharedObjects + class Player(bs.Player['Team']): def __init__(self) -> None: @@ -35,7 +36,7 @@ class Team(bs.Team[Player]): # ba_meta export bascenev1.GameActivity class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): - name = 'Vanishing Tiles' + name = 'Vanishing Tiles' description = 'Tiles disappear one by one. Be the last one standing!' scoreconfig = bs.ScoreConfig(label='Survived', scoretype=bs.ScoreType.MILLISECONDS) announce_player_deaths = True @@ -57,19 +58,19 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def __init__(self, settings: dict) -> None: super().__init__(settings) - self._epic_mode = bool(settings.get('Epic Mode', False)) - self._show_credits = bool(settings.get('Show Credits', True)) + self._epic_mode: bool = bool(settings.get('Epic Mode', False)) + self._show_credits: bool = bool(settings.get('Show Credits', True)) self.default_music = bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL - self._round = 1 - self._removing = False + self._round: int = 1 + self._removing: bool = False self._game_start_time: Optional[float] = None self._tile_nodes: Dict[int, bs.Node] = {} self._region_nodes: Dict[int, bs.Node] = {} - self._present_tile_ids: set[int] = set() + self._present_tile_ids: set[int] = set() - self._remove_speed = 1.8 + self._remove_speed: float = 1.8 self._collide_mat = bs.Material() self._collide_mat.add_actions(actions=(('modify_part_collision', 'collide', True),)) @@ -77,8 +78,8 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._no_collide_mat.add_actions(actions=(('modify_part_collision', 'collide', False),)) self._default_tex = bs.gettexture('powerupHealth') - self._warn_tex = bs.gettexture('powerupCurse') - self._final_tex = bs.gettexture('powerupPunch') + self._warn_tex = bs.gettexture('powerupCurse') + self._final_tex = bs.gettexture('powerupPunch') self._hud_round: Optional[bs.Node] = None self._hud_players: Optional[bs.Node] = None @@ -103,7 +104,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): if self._show_credits: self._credit_node = bs.newnode('text', attrs={ - 'text': 'Made by Mr.Paradox & senchx', + 'text': 'Made by Mr.Ghosty', 'scale': 0.7, 'position': (0, 8), 'shadow': 0.8, 'flatness': 1.0, 'color': (1.0, 0.3, 0.8, 1.0), 'h_align': 'center', 'v_attach': 'bottom', }) @@ -162,7 +163,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._hud_players.text = f'Players alive: {alive}' if self._hud_tiles: self._hud_tiles.color = (1.0, 0.3, 0.3, 1) if urgent else (0.5, 1.0, 0.5, 1) - self._hud_tiles.text = f'Tiles left: {tiles}' + self._hud_tiles.text = f'Tiles left: {tiles}' def _show_round_banner(self) -> None: node = bs.newnode('text', attrs={ @@ -194,7 +195,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): (-1.5, 2, -9), (-1.5, 2, -6), (-1.5, 2, -3), (-1.5, 2, 0), (-4.5, 2, -9), (-4.5, 2, -6), (-4.5, 2, -3), (-4.5, 2, 0), ] - model = bs.getmesh('buttonSquareOpaque') + model = bs.getmesh('buttonSquareOpaque') shared = SharedObjects.get() for i, pos in enumerate(positions): tile = bs.newnode('prop', attrs={ @@ -207,7 +208,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): 'position': pos, 'scale': (3.5, 0.1, 3.5), 'type': 'box', 'materials': [self._collide_mat, shared.footing_material], }) - self._tile_nodes[i] = tile + self._tile_nodes[i] = tile self._region_nodes[i] = region self._present_tile_ids.add(i) self._refresh_hud() @@ -220,7 +221,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def _make_final_tile(self) -> None: try: tile_id = list(self._present_tile_ids)[0] - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) if tile and tile.exists(): tile.color_texture = self._final_tex except Exception: @@ -247,7 +248,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): bs.timer(self._remove_speed, self._remove_next_tile) def _remove_tile(self, tile_id: int) -> None: - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) region = self._region_nodes.get(tile_id) if tile is None or not tile.exists(): self._present_tile_ids.discard(tile_id) @@ -344,12 +345,12 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): if self.has_ended(): return cur_time = bs.time() - start = self._game_start_time or cur_time - results = bs.GameResults() + start = self._game_start_time or cur_time + results = bs.GameResults() for team in self.teams: longest = 0.0 for p in team.players: - death = p.death_time or (cur_time + 1) + death = p.death_time or (cur_time + 1) longest = max(longest, death - start) results.set_team_score(team, int(longest * 1000)) @@ -377,7 +378,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): class VanishingTilesMapDefs: points = {'spawn1': (0, 3, -5)} - boxes = { + boxes = { 'area_of_interest_bounds': (0, 4, -5, 0, 0, 0, 16, 8, 16), 'map_bounds': (0, 4, -5, 0, 0, 0, 30, 14, 30), } @@ -403,10 +404,7 @@ class VanishingTilesMap(bs.Map): }) -try: - bs.register_map(VanishingTilesMap) -except Exception: - bs._map.register_map(VanishingTilesMap) +bs._map.register_map(VanishingTilesMap) # ba_meta export babase.Plugin From 0749053077e34f92abe2148f14b90c82deaa758b Mon Sep 17 00:00:00 2001 From: MrParadox1691 <292215964+MrParadox1691@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:12:12 +0000 Subject: [PATCH 74/84] [ci] apply-plugin-metadata-and-formatting --- plugins/minigames.json | 5 ++-- plugins/minigames/vanishing_tiles.py | 45 ++++++++++++++-------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index efc4c40..50e8787 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1694,8 +1694,8 @@ "external_url": "https://discord.gg/sQGDsztQcy", "authors": [ { - "name": "Mr.Paradox", - "email": "microsoftusergame@gmail.com", + "name": "Mr.ghosty", + "email": "", "discord": "gaurangbroyo" }, { @@ -1705,6 +1705,7 @@ } ], "versions": { + "2.1.0": null, "1.0.2": { "api_version": 9, "commit_sha": "edaed70", diff --git a/plugins/minigames/vanishing_tiles.py b/plugins/minigames/vanishing_tiles.py index fd191fe..5a02884 100644 --- a/plugins/minigames/vanishing_tiles.py +++ b/plugins/minigames/vanishing_tiles.py @@ -1,5 +1,10 @@ # ba_meta require api 9 from __future__ import annotations +from bascenev1lib.gameutils import SharedObjects +import bascenev1 as bs +import babase +import random +from typing import Optional, List, Dict, Any # Vanishing Tiles - BombSquad Minigame # Tiles disappear one by one. Survive all rounds to win. @@ -16,12 +21,6 @@ plugman = dict( version="2.1.0", ) -from typing import Optional, List, Dict, Any -import random -import babase -import bascenev1 as bs -from bascenev1lib.gameutils import SharedObjects - class Player(bs.Player['Team']): def __init__(self) -> None: @@ -36,7 +35,7 @@ class Team(bs.Team[Player]): # ba_meta export bascenev1.GameActivity class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): - name = 'Vanishing Tiles' + name = 'Vanishing Tiles' description = 'Tiles disappear one by one. Be the last one standing!' scoreconfig = bs.ScoreConfig(label='Survived', scoretype=bs.ScoreType.MILLISECONDS) announce_player_deaths = True @@ -58,17 +57,17 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def __init__(self, settings: dict) -> None: super().__init__(settings) - self._epic_mode: bool = bool(settings.get('Epic Mode', False)) - self._show_credits: bool = bool(settings.get('Show Credits', True)) + self._epic_mode: bool = bool(settings.get('Epic Mode', False)) + self._show_credits: bool = bool(settings.get('Show Credits', True)) self.default_music = bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL - self._round: int = 1 - self._removing: bool = False + self._round: int = 1 + self._removing: bool = False self._game_start_time: Optional[float] = None self._tile_nodes: Dict[int, bs.Node] = {} self._region_nodes: Dict[int, bs.Node] = {} - self._present_tile_ids: set[int] = set() + self._present_tile_ids: set[int] = set() self._remove_speed: float = 1.8 @@ -78,8 +77,8 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._no_collide_mat.add_actions(actions=(('modify_part_collision', 'collide', False),)) self._default_tex = bs.gettexture('powerupHealth') - self._warn_tex = bs.gettexture('powerupCurse') - self._final_tex = bs.gettexture('powerupPunch') + self._warn_tex = bs.gettexture('powerupCurse') + self._final_tex = bs.gettexture('powerupPunch') self._hud_round: Optional[bs.Node] = None self._hud_players: Optional[bs.Node] = None @@ -163,7 +162,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._hud_players.text = f'Players alive: {alive}' if self._hud_tiles: self._hud_tiles.color = (1.0, 0.3, 0.3, 1) if urgent else (0.5, 1.0, 0.5, 1) - self._hud_tiles.text = f'Tiles left: {tiles}' + self._hud_tiles.text = f'Tiles left: {tiles}' def _show_round_banner(self) -> None: node = bs.newnode('text', attrs={ @@ -195,7 +194,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): (-1.5, 2, -9), (-1.5, 2, -6), (-1.5, 2, -3), (-1.5, 2, 0), (-4.5, 2, -9), (-4.5, 2, -6), (-4.5, 2, -3), (-4.5, 2, 0), ] - model = bs.getmesh('buttonSquareOpaque') + model = bs.getmesh('buttonSquareOpaque') shared = SharedObjects.get() for i, pos in enumerate(positions): tile = bs.newnode('prop', attrs={ @@ -208,7 +207,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): 'position': pos, 'scale': (3.5, 0.1, 3.5), 'type': 'box', 'materials': [self._collide_mat, shared.footing_material], }) - self._tile_nodes[i] = tile + self._tile_nodes[i] = tile self._region_nodes[i] = region self._present_tile_ids.add(i) self._refresh_hud() @@ -221,7 +220,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def _make_final_tile(self) -> None: try: tile_id = list(self._present_tile_ids)[0] - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) if tile and tile.exists(): tile.color_texture = self._final_tex except Exception: @@ -248,7 +247,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): bs.timer(self._remove_speed, self._remove_next_tile) def _remove_tile(self, tile_id: int) -> None: - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) region = self._region_nodes.get(tile_id) if tile is None or not tile.exists(): self._present_tile_ids.discard(tile_id) @@ -345,12 +344,12 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): if self.has_ended(): return cur_time = bs.time() - start = self._game_start_time or cur_time - results = bs.GameResults() + start = self._game_start_time or cur_time + results = bs.GameResults() for team in self.teams: longest = 0.0 for p in team.players: - death = p.death_time or (cur_time + 1) + death = p.death_time or (cur_time + 1) longest = max(longest, death - start) results.set_team_score(team, int(longest * 1000)) @@ -378,7 +377,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): class VanishingTilesMapDefs: points = {'spawn1': (0, 3, -5)} - boxes = { + boxes = { 'area_of_interest_bounds': (0, 4, -5, 0, 0, 0, 16, 8, 16), 'map_bounds': (0, 4, -5, 0, 0, 0, 30, 14, 30), } From 18dbcbbe3ff3e471680d6831acd65205a1601b3d Mon Sep 17 00:00:00 2001 From: MrParadox1691 <292215964+MrParadox1691@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:12:15 +0000 Subject: [PATCH 75/84] [ci] apply-version-metadata --- plugins/minigames.json | 7 ++++++- plugins/utilities.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index 50e8787..ddf32fa 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1705,7 +1705,12 @@ } ], "versions": { - "2.1.0": null, + "2.1.0": { + "api_version": 9, + "commit_sha": "0749053", + "released_on": "10-06-2026", + "md5sum": "152e2e149b2686619c44aa7880a7e460" + }, "1.0.2": { "api_version": 9, "commit_sha": "edaed70", diff --git a/plugins/utilities.json b/plugins/utilities.json index 42da57d..31509f5 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2566,4 +2566,4 @@ } } } -} +} \ No newline at end of file From 767fbe3a28dc06c36ab64d6c90f276f6d8941862 Mon Sep 17 00:00:00 2001 From: Loup <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:02:11 +0530 Subject: [PATCH 76/84] Delete version 1.0.2 from minigames.json Removed version 1.0.2 details from minigames.json --- plugins/minigames.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index ddf32fa..b574ae3 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1710,14 +1710,8 @@ "commit_sha": "0749053", "released_on": "10-06-2026", "md5sum": "152e2e149b2686619c44aa7880a7e460" - }, - "1.0.2": { - "api_version": 9, - "commit_sha": "edaed70", - "released_on": "10-06-2026", - "md5sum": "3711549cfb7a5225bbe385b848f070e0" } } } } -} \ No newline at end of file +} From 3adfe38e55c21ae583658ed6829d1e3c2f74c171 Mon Sep 17 00:00:00 2001 From: GeoHnlol <107043540+GeoHnlol@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:35:04 +0300 Subject: [PATCH 77/84] Changed version number to 1.1.9 --- CHANGELOG.md | 4 ++++ index.json | 1 + plugin_manager.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45cca48..b305373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Plugin Manager (dd-mm-yyyy) +### 1.1.9 (11-06-2026) + +- Fix for bs 1.7.63 + ### 1.1.8 (23-02-2026) - Fix for bs 1.7.61 build no 22714 diff --git a/index.json b/index.json index f9bc733..33d20ef 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,7 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { + "1.1.9": null, "1.1.8": { "api_version": 9, "commit_sha": "8222081", diff --git a/plugin_manager.py b/plugin_manager.py index 645df56..5fb639d 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -26,7 +26,7 @@ from datetime import datetime # Modules used for overriding AllSettingsWindow import logging -PLUGIN_MANAGER_VERSION = "1.1.8" +PLUGIN_MANAGER_VERSION = "1.1.9" 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. From d0df37d7641fb296be5eb8f212d2b2d3405b8f48 Mon Sep 17 00:00:00 2001 From: GeoHnlol <107043540+GeoHnlol@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:35:34 +0000 Subject: [PATCH 78/84] [ci] apply-version-metadata --- index.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.json b/index.json index 33d20ef..1c88a45 100644 --- a/index.json +++ b/index.json @@ -1,7 +1,12 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { - "1.1.9": null, + "1.1.9": { + "api_version": 9, + "commit_sha": "3adfe38", + "released_on": "11-06-2026", + "md5sum": "d7e990ea4e5c530f780e617c9bdafb42" + }, "1.1.8": { "api_version": 9, "commit_sha": "8222081", From 643ac51a08a3c304f1a21a53465f3cbd6ae7a9af Mon Sep 17 00:00:00 2001 From: "Mr.Paradox" Date: Thu, 11 Jun 2026 20:23:25 +0530 Subject: [PATCH 79/84] Update vanishing_tiles.py --- plugins/minigames/vanishing_tiles.py | 63 ++++++++++++++-------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/plugins/minigames/vanishing_tiles.py b/plugins/minigames/vanishing_tiles.py index 5a02884..d17867b 100644 --- a/plugins/minigames/vanishing_tiles.py +++ b/plugins/minigames/vanishing_tiles.py @@ -1,10 +1,5 @@ # ba_meta require api 9 from __future__ import annotations -from bascenev1lib.gameutils import SharedObjects -import bascenev1 as bs -import babase -import random -from typing import Optional, List, Dict, Any # Vanishing Tiles - BombSquad Minigame # Tiles disappear one by one. Survive all rounds to win. @@ -15,12 +10,18 @@ plugman = dict( description="Tiles disappear gradually. Survive each round to continue. Last player standing wins!", external_url="https://discord.gg/sQGDsztQcy", authors=[ - {"name": "Mr.ghosty", "email": "", "discord": "gaurangbroyo"}, + {"name": "Mr.Paradox", "email": "", "discord": "gaurangbroyo"}, {"name": "senchx", "email": "", "discord": "senchx0"}, ], - version="2.1.0", + version="2.1.1", ) +from typing import Optional, List, Dict, Any +import random +import babase +import bascenev1 as bs +from bascenev1lib.gameutils import SharedObjects + class Player(bs.Player['Team']): def __init__(self) -> None: @@ -35,7 +36,7 @@ class Team(bs.Team[Player]): # ba_meta export bascenev1.GameActivity class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): - name = 'Vanishing Tiles' + name = 'Vanishing Tiles' description = 'Tiles disappear one by one. Be the last one standing!' scoreconfig = bs.ScoreConfig(label='Survived', scoretype=bs.ScoreType.MILLISECONDS) announce_player_deaths = True @@ -57,17 +58,17 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def __init__(self, settings: dict) -> None: super().__init__(settings) - self._epic_mode: bool = bool(settings.get('Epic Mode', False)) - self._show_credits: bool = bool(settings.get('Show Credits', True)) + self._epic_mode: bool = bool(settings.get('Epic Mode', False)) + self._show_credits: bool = bool(settings.get('Show Credits', True)) self.default_music = bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL - self._round: int = 1 - self._removing: bool = False + self._round: int = 1 + self._removing: bool = False self._game_start_time: Optional[float] = None self._tile_nodes: Dict[int, bs.Node] = {} self._region_nodes: Dict[int, bs.Node] = {} - self._present_tile_ids: set[int] = set() + self._present_tile_ids: set[int] = set() self._remove_speed: float = 1.8 @@ -77,8 +78,8 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._no_collide_mat.add_actions(actions=(('modify_part_collision', 'collide', False),)) self._default_tex = bs.gettexture('powerupHealth') - self._warn_tex = bs.gettexture('powerupCurse') - self._final_tex = bs.gettexture('powerupPunch') + self._warn_tex = bs.gettexture('powerupCurse') + self._final_tex = bs.gettexture('powerupPunch') self._hud_round: Optional[bs.Node] = None self._hud_players: Optional[bs.Node] = None @@ -103,7 +104,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): if self._show_credits: self._credit_node = bs.newnode('text', attrs={ - 'text': 'Made by Mr.Ghosty', + 'text': 'Made by Mr.Paradox', 'scale': 0.7, 'position': (0, 8), 'shadow': 0.8, 'flatness': 1.0, 'color': (1.0, 0.3, 0.8, 1.0), 'h_align': 'center', 'v_attach': 'bottom', }) @@ -162,7 +163,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._hud_players.text = f'Players alive: {alive}' if self._hud_tiles: self._hud_tiles.color = (1.0, 0.3, 0.3, 1) if urgent else (0.5, 1.0, 0.5, 1) - self._hud_tiles.text = f'Tiles left: {tiles}' + self._hud_tiles.text = f'Tiles left: {tiles}' def _show_round_banner(self) -> None: node = bs.newnode('text', attrs={ @@ -194,7 +195,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): (-1.5, 2, -9), (-1.5, 2, -6), (-1.5, 2, -3), (-1.5, 2, 0), (-4.5, 2, -9), (-4.5, 2, -6), (-4.5, 2, -3), (-4.5, 2, 0), ] - model = bs.getmesh('buttonSquareOpaque') + model = bs.getmesh('buttonSquareOpaque') shared = SharedObjects.get() for i, pos in enumerate(positions): tile = bs.newnode('prop', attrs={ @@ -207,7 +208,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): 'position': pos, 'scale': (3.5, 0.1, 3.5), 'type': 'box', 'materials': [self._collide_mat, shared.footing_material], }) - self._tile_nodes[i] = tile + self._tile_nodes[i] = tile self._region_nodes[i] = region self._present_tile_ids.add(i) self._refresh_hud() @@ -220,7 +221,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def _make_final_tile(self) -> None: try: tile_id = list(self._present_tile_ids)[0] - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) if tile and tile.exists(): tile.color_texture = self._final_tex except Exception: @@ -247,7 +248,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): bs.timer(self._remove_speed, self._remove_next_tile) def _remove_tile(self, tile_id: int) -> None: - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) region = self._region_nodes.get(tile_id) if tile is None or not tile.exists(): self._present_tile_ids.discard(tile_id) @@ -344,12 +345,12 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): if self.has_ended(): return cur_time = bs.time() - start = self._game_start_time or cur_time - results = bs.GameResults() + start = self._game_start_time or cur_time + results = bs.GameResults() for team in self.teams: longest = 0.0 for p in team.players: - death = p.death_time or (cur_time + 1) + death = p.death_time or (cur_time + 1) longest = max(longest, death - start) results.set_team_score(team, int(longest * 1000)) @@ -377,7 +378,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): class VanishingTilesMapDefs: points = {'spawn1': (0, 3, -5)} - boxes = { + boxes = { 'area_of_interest_bounds': (0, 4, -5, 0, 0, 0, 16, 8, 16), 'map_bounds': (0, 4, -5, 0, 0, 0, 30, 14, 30), } @@ -403,14 +404,12 @@ class VanishingTilesMap(bs.Map): }) -bs._map.register_map(VanishingTilesMap) +try: + bs._map.register_map(VanishingTilesMap) +except Exception: + pass # ba_meta export babase.Plugin class Main(babase.Plugin): - def __init__(self) -> None: - babase.app.classic.add_coop_practice_level(bs.Level( - name='Vanishing Tiles', displayname='Vanishing Tiles', - gametype=VanishingTilesGame, settings={}, - preview_texture_name='powerupHealth', - )) + pass From b8dee8f224334113ca1ce48682e9a09a8f370348 Mon Sep 17 00:00:00 2001 From: "Mr.Paradox" Date: Thu, 11 Jun 2026 20:43:39 +0530 Subject: [PATCH 80/84] Update vanishing_tiles.py --- plugins/minigames/vanishing_tiles.py | 45 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/plugins/minigames/vanishing_tiles.py b/plugins/minigames/vanishing_tiles.py index d17867b..5a75efe 100644 --- a/plugins/minigames/vanishing_tiles.py +++ b/plugins/minigames/vanishing_tiles.py @@ -1,5 +1,10 @@ # ba_meta require api 9 from __future__ import annotations +from bascenev1lib.gameutils import SharedObjects +import bascenev1 as bs +import babase +import random +from typing import Optional, List, Dict, Any # Vanishing Tiles - BombSquad Minigame # Tiles disappear one by one. Survive all rounds to win. @@ -16,12 +21,6 @@ plugman = dict( version="2.1.1", ) -from typing import Optional, List, Dict, Any -import random -import babase -import bascenev1 as bs -from bascenev1lib.gameutils import SharedObjects - class Player(bs.Player['Team']): def __init__(self) -> None: @@ -36,7 +35,7 @@ class Team(bs.Team[Player]): # ba_meta export bascenev1.GameActivity class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): - name = 'Vanishing Tiles' + name = 'Vanishing Tiles' description = 'Tiles disappear one by one. Be the last one standing!' scoreconfig = bs.ScoreConfig(label='Survived', scoretype=bs.ScoreType.MILLISECONDS) announce_player_deaths = True @@ -58,17 +57,17 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def __init__(self, settings: dict) -> None: super().__init__(settings) - self._epic_mode: bool = bool(settings.get('Epic Mode', False)) - self._show_credits: bool = bool(settings.get('Show Credits', True)) + self._epic_mode: bool = bool(settings.get('Epic Mode', False)) + self._show_credits: bool = bool(settings.get('Show Credits', True)) self.default_music = bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL - self._round: int = 1 - self._removing: bool = False + self._round: int = 1 + self._removing: bool = False self._game_start_time: Optional[float] = None self._tile_nodes: Dict[int, bs.Node] = {} self._region_nodes: Dict[int, bs.Node] = {} - self._present_tile_ids: set[int] = set() + self._present_tile_ids: set[int] = set() self._remove_speed: float = 1.8 @@ -78,8 +77,8 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._no_collide_mat.add_actions(actions=(('modify_part_collision', 'collide', False),)) self._default_tex = bs.gettexture('powerupHealth') - self._warn_tex = bs.gettexture('powerupCurse') - self._final_tex = bs.gettexture('powerupPunch') + self._warn_tex = bs.gettexture('powerupCurse') + self._final_tex = bs.gettexture('powerupPunch') self._hud_round: Optional[bs.Node] = None self._hud_players: Optional[bs.Node] = None @@ -163,7 +162,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): self._hud_players.text = f'Players alive: {alive}' if self._hud_tiles: self._hud_tiles.color = (1.0, 0.3, 0.3, 1) if urgent else (0.5, 1.0, 0.5, 1) - self._hud_tiles.text = f'Tiles left: {tiles}' + self._hud_tiles.text = f'Tiles left: {tiles}' def _show_round_banner(self) -> None: node = bs.newnode('text', attrs={ @@ -195,7 +194,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): (-1.5, 2, -9), (-1.5, 2, -6), (-1.5, 2, -3), (-1.5, 2, 0), (-4.5, 2, -9), (-4.5, 2, -6), (-4.5, 2, -3), (-4.5, 2, 0), ] - model = bs.getmesh('buttonSquareOpaque') + model = bs.getmesh('buttonSquareOpaque') shared = SharedObjects.get() for i, pos in enumerate(positions): tile = bs.newnode('prop', attrs={ @@ -208,7 +207,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): 'position': pos, 'scale': (3.5, 0.1, 3.5), 'type': 'box', 'materials': [self._collide_mat, shared.footing_material], }) - self._tile_nodes[i] = tile + self._tile_nodes[i] = tile self._region_nodes[i] = region self._present_tile_ids.add(i) self._refresh_hud() @@ -221,7 +220,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): def _make_final_tile(self) -> None: try: tile_id = list(self._present_tile_ids)[0] - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) if tile and tile.exists(): tile.color_texture = self._final_tex except Exception: @@ -248,7 +247,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): bs.timer(self._remove_speed, self._remove_next_tile) def _remove_tile(self, tile_id: int) -> None: - tile = self._tile_nodes.get(tile_id) + tile = self._tile_nodes.get(tile_id) region = self._region_nodes.get(tile_id) if tile is None or not tile.exists(): self._present_tile_ids.discard(tile_id) @@ -345,12 +344,12 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): if self.has_ended(): return cur_time = bs.time() - start = self._game_start_time or cur_time - results = bs.GameResults() + start = self._game_start_time or cur_time + results = bs.GameResults() for team in self.teams: longest = 0.0 for p in team.players: - death = p.death_time or (cur_time + 1) + death = p.death_time or (cur_time + 1) longest = max(longest, death - start) results.set_team_score(team, int(longest * 1000)) @@ -378,7 +377,7 @@ class VanishingTilesGame(bs.TeamGameActivity[Player, Team]): class VanishingTilesMapDefs: points = {'spawn1': (0, 3, -5)} - boxes = { + boxes = { 'area_of_interest_bounds': (0, 4, -5, 0, 0, 0, 16, 8, 16), 'map_bounds': (0, 4, -5, 0, 0, 0, 30, 14, 30), } From 5d7d22245bd02765fcc422ee6a0abe240565379e Mon Sep 17 00:00:00 2001 From: MrParadox1691 <292215964+MrParadox1691@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:16:42 +0000 Subject: [PATCH 81/84] [ci] apply-plugin-metadata-and-formatting --- plugins/minigames.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index b574ae3..05c0c8e 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1694,7 +1694,7 @@ "external_url": "https://discord.gg/sQGDsztQcy", "authors": [ { - "name": "Mr.ghosty", + "name": "Mr.Paradox", "email": "", "discord": "gaurangbroyo" }, @@ -1705,6 +1705,7 @@ } ], "versions": { + "2.1.1": null, "2.1.0": { "api_version": 9, "commit_sha": "0749053", @@ -1714,4 +1715,4 @@ } } } -} +} \ No newline at end of file From f1c02ae85a17d531e9004373d445825b1f719c31 Mon Sep 17 00:00:00 2001 From: MrParadox1691 <292215964+MrParadox1691@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:16:45 +0000 Subject: [PATCH 82/84] [ci] apply-version-metadata --- plugins/minigames.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/minigames.json b/plugins/minigames.json index 05c0c8e..932471c 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1705,7 +1705,12 @@ } ], "versions": { - "2.1.1": null, + "2.1.1": { + "api_version": 9, + "commit_sha": "5d7d222", + "released_on": "11-06-2026", + "md5sum": "d71b9615872ba3655a83166dd211ecd2" + }, "2.1.0": { "api_version": 9, "commit_sha": "0749053", From a1baa5fca211cae9efa98ee14f4ed23556ed089b Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:10:44 +0530 Subject: [PATCH 83/84] fix for 1.1.9 breaking on older versions --- CHANGELOG.md | 4 ++++ index.json | 1 + plugin_manager.py | 5 ++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b305373..9390c7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Plugin Manager (dd-mm-yyyy) +### 1.1.10 (12-06-2026) + +- Fix for older bs versions using `EXPORT_CLASS_NAME_SHORTCUTS` import + ### 1.1.9 (11-06-2026) - Fix for bs 1.7.63 diff --git a/index.json b/index.json index 1c88a45..5f90c12 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,7 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { + "1.1.10": null, "1.1.9": { "api_version": 9, "commit_sha": "3adfe38", diff --git a/plugin_manager.py b/plugin_manager.py index 5fb639d..8d3300d 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -2,7 +2,6 @@ import babase import bauiv1 as bui from bauiv1lib import popup, confirm -from babase._meta import _DEPRECATED_EXPORT_SHORTCUTS from bauiv1lib.settings.allsettings import AllSettingsWindow import urllib.request @@ -26,7 +25,7 @@ from datetime import datetime # Modules used for overriding AllSettingsWindow import logging -PLUGIN_MANAGER_VERSION = "1.1.9" +PLUGIN_MANAGER_VERSION = "1.1.10" 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. @@ -82,7 +81,7 @@ REGEXP = { "plugin_entry_points": re.compile( bytes( "(ba_meta export (plugin|{})\n+class )(.*)\\(".format( - _regexp_friendly_class_name_shortcut(_DEPRECATED_EXPORT_SHORTCUTS["plugin"]), + _regexp_friendly_class_name_shortcut("babase.Plugin"), ), "utf-8" ), From 7acd7a87abe146fc151f21969bf7156ec5b56d2b Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:41:53 +0000 Subject: [PATCH 84/84] [ci] apply-version-metadata --- index.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.json b/index.json index 5f90c12..120a1ba 100644 --- a/index.json +++ b/index.json @@ -1,7 +1,12 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { - "1.1.10": null, + "1.1.10": { + "api_version": 9, + "commit_sha": "a1baa5f", + "released_on": "12-06-2026", + "md5sum": "ee4f7e3e88c2982bc8290e204b10b050" + }, "1.1.9": { "api_version": 9, "commit_sha": "3adfe38",