From 7162f9943f314e4b58c62235cde3eb5d00bcf491 Mon Sep 17 00:00:00 2001 From: DinoWattz <116862698+DinoWattz@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:24:52 -0300 Subject: [PATCH 1/7] 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 00000000..2620c796 --- /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 00000000..9a13f2d0 --- /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="ChatBubbles", + 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 00000000..6d120833 --- /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 00000000..e7398cf2 --- /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 04ea0b2b94d5e3eea83e23bf1cbad67b26d89639 Mon Sep 17 00:00:00 2001 From: DinoWattz <116862698+DinoWattz@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:55:29 -0300 Subject: [PATCH 2/7] snake_case --- plugins/utilities/better_camera_shake.py | 2 +- plugins/utilities/chat_bubbles.py | 2 +- plugins/utilities/enhanced_effects.py | 2 +- plugins/utilities/sleep_on_afk.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/utilities/better_camera_shake.py b/plugins/utilities/better_camera_shake.py index 2620c796..634fb1a4 100644 --- a/plugins/utilities/better_camera_shake.py +++ b/plugins/utilities/better_camera_shake.py @@ -16,7 +16,7 @@ from typing import Any, Sequence plugman = dict( - plugin_name="Better Camera Shake", + 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=[ diff --git a/plugins/utilities/chat_bubbles.py b/plugins/utilities/chat_bubbles.py index 9a13f2d0..96899054 100644 --- a/plugins/utilities/chat_bubbles.py +++ b/plugins/utilities/chat_bubbles.py @@ -14,7 +14,7 @@ from typing import Any plugman = dict( - plugin_name="ChatBubbles", + plugin_name="chatbubbles", description="Adds whatever the players say above their character, includes a few other features too!", external_url="", authors=[ diff --git a/plugins/utilities/enhanced_effects.py b/plugins/utilities/enhanced_effects.py index 6d120833..9a33534f 100644 --- a/plugins/utilities/enhanced_effects.py +++ b/plugins/utilities/enhanced_effects.py @@ -14,7 +14,7 @@ from typing import Any plugman = dict( - plugin_name="Enhanced Effects", + 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=[ diff --git a/plugins/utilities/sleep_on_afk.py b/plugins/utilities/sleep_on_afk.py index e7398cf2..7c14354d 100644 --- a/plugins/utilities/sleep_on_afk.py +++ b/plugins/utilities/sleep_on_afk.py @@ -7,7 +7,7 @@ from bascenev1lib.actor.spaz import Spaz plugman = dict( - plugin_name="Sleep On AFK", + plugin_name="sleep_on_afk", description="Staying idle for 40 seconds will make your character fall asleep, they need rest too..", external_url="", authors=[ From 77860058859995200d5ca037a4d680cf77a9b30a Mon Sep 17 00:00:00 2001 From: DinoWattz <116862698+DinoWattz@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:59:50 -0300 Subject: [PATCH 3/7] rename chatbubbles --- plugins/utilities/chat_bubbles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/utilities/chat_bubbles.py b/plugins/utilities/chat_bubbles.py index 96899054..446ad07b 100644 --- a/plugins/utilities/chat_bubbles.py +++ b/plugins/utilities/chat_bubbles.py @@ -14,7 +14,7 @@ from typing import Any plugman = dict( - plugin_name="chatbubbles", + plugin_name="chat_bubbles", description="Adds whatever the players say above their character, includes a few other features too!", external_url="", authors=[ From c0a6e8e4e5d75fa9649fd45586e8acb8c24467cf Mon Sep 17 00:00:00 2001 From: DinoWattz <116862698+DinoWattz@users.noreply.github.com> Date: Mon, 23 Feb 2026 05:09:50 -0300 Subject: [PATCH 4/7] beep --- plugins/utilities/chat_bubbles.py | 582 ------------------------------ 1 file changed, 582 deletions(-) delete mode 100644 plugins/utilities/chat_bubbles.py diff --git a/plugins/utilities/chat_bubbles.py b/plugins/utilities/chat_bubbles.py deleted file mode 100644 index 446ad07b..00000000 --- a/plugins/utilities/chat_bubbles.py +++ /dev/null @@ -1,582 +0,0 @@ -# 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) From ebfad79f51966f2ba3762dce370160a4b2570f60 Mon Sep 17 00:00:00 2001 From: DinoWattz <116862698+DinoWattz@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:10:23 +0000 Subject: [PATCH 5/7] [ci] apply-plugin-metadata-and-formatting --- plugins/utilities.json | 42 ++ plugins/utilities/better_camera_shake.py | 599 ++++++++++++----------- plugins/utilities/enhanced_effects.py | 285 ++++++----- plugins/utilities/sleep_on_afk.py | 67 ++- 4 files changed, 535 insertions(+), 458 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 0c62bac0..ac7a87c3 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -2426,6 +2426,48 @@ "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 + } + }, + "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 634fb1a4..d176f46a 100644 --- a/plugins/utilities/better_camera_shake.py +++ b/plugins/utilities/better_camera_shake.py @@ -26,356 +26,363 @@ ) # 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', - ) +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(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': + + def emit() -> None: + bs.emitfx( + position=position, + velocity=velocity, + count=30, + spread=2.0, + scale=0.4, + chunk_type='ice', + emit_type='stickers', + ) - # And emit some shrapnel. - if self.blast_type == 'ice': + # It looks better if we delay a bit. + bs.timer(0.05, emit) - def emit() -> None: - bs.emitfx( - position=position, - velocity=velocity, - count=30, - spread=2.0, - scale=0.4, - chunk_type='ice', - emit_type='stickers', - ) + elif self.blast_type == 'sticky': - # It looks better if we delay a bit. - bs.timer(0.05, emit) + 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', + ) - elif self.blast_type == 'sticky': + # It looks better if we delay a bit. + bs.timer(0.05, emit) - 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', - ) + elif self.blast_type == 'impact': - # It looks better if we delay a bit. - bs.timer(0.05, emit) + 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', + ) - elif self.blast_type == 'impact': + # It looks better if we delay a bit. + bs.timer(0.05, emit) - def emit() -> None: + 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), - scale=0.8, - chunk_type='metal', + chunk_type='rock', ) 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', + 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', + ) - # It looks better if we delay a bit. - bs.timer(0.05, emit) - - else: # Regular or land mine bomb shrapnel. + # TNT throws splintery chunks. + if self.blast_type == 'tnt': - 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': - - 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 + 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) - 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/enhanced_effects.py b/plugins/utilities/enhanced_effects.py index 9a33534f..d78f5616 100644 --- a/plugins/utilities/enhanced_effects.py +++ b/plugins/utilities/enhanced_effects.py @@ -25,92 +25,95 @@ # 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, - ) - 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} ) - 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} + for attr in ['input0', 'input1']: + bs.animate( + self.cmb, + attr, + {0.0: 0.693, 0.05: 0.7, 0.5: 0.693}, + 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) + 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() @@ -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 linear_to_srgb(c: float) -> float: 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 @@ def on_app_running(self) -> None: 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 @@ def new_init(self: Blast, blast_type='normal', hit_type='explosion', *args, **kw # 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 @@ def new_init(self: Blast, blast_type='normal', hit_type='explosion', *args, **kw # 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 @@ 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 + 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', + ) - original_color = delegate._original_color - original_highlight = delegate._original_highlight + 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 - - 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 + 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 diff --git a/plugins/utilities/sleep_on_afk.py b/plugins/utilities/sleep_on_afk.py index 7c14354d..3f3ad3b3 100644 --- a/plugins/utilities/sleep_on_afk.py +++ b/plugins/utilities/sleep_on_afk.py @@ -16,28 +16,35 @@ 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 9d0cf02ad6782fd6e13d0199ead151fb9945f949 Mon Sep 17 00:00:00 2001 From: DinoWattz <116862698+DinoWattz@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:10:24 +0000 Subject: [PATCH 6/7] [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 ac7a87c3..5d6e1f1e 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": "ebfad79", + "released_on": "23-02-2026", + "md5sum": "552b0ff0492bdab6cf69c1bc2d679e0a" + } } }, "enhanced_effects": { @@ -2452,7 +2457,12 @@ } ], "versions": { - "1.0.0": null + "1.0.0": { + "api_version": 9, + "commit_sha": "ebfad79", + "released_on": "23-02-2026", + "md5sum": "63c665905dd4619d3f99ca8977f8fbf3" + } } }, "sleep_on_afk": { @@ -2466,7 +2476,12 @@ } ], "versions": { - "1.0.0": null + "1.0.0": { + "api_version": 9, + "commit_sha": "ebfad79", + "released_on": "23-02-2026", + "md5sum": "5de7a474c3c6b21e8ea1e1abff3c07a1" + } } } } From 2be0b496307a3908cd0bc2257643d115bbb6f718 Mon Sep 17 00:00:00 2001 From: DinoWattz <116862698+DinoWattz@users.noreply.github.com> Date: Mon, 23 Feb 2026 05:11:08 -0300 Subject: [PATCH 7/7] bop --- plugins/utilities/chat_bubbles.py | 582 ++++++++++++++++++++++++++++++ 1 file changed, 582 insertions(+) create mode 100644 plugins/utilities/chat_bubbles.py diff --git a/plugins/utilities/chat_bubbles.py b/plugins/utilities/chat_bubbles.py new file mode 100644 index 00000000..446ad07b --- /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)