Merge pull request #444 from DinoWattz/main

Add better_camera_shake, chat_bubbles, enhanced_effects and sleep_on_afk
This commit is contained in:
Loup 2026-03-09 23:14:05 +05:30 committed by GitHub
commit 4baad2b813
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1497 additions and 0 deletions

View file

@ -2426,6 +2426,82 @@
"md5sum": "d8002dfc93be74d71f8e6e4aacfe6fd1" "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": {
"api_version": 9,
"commit_sha": "e67d891",
"released_on": "23-02-2026",
"md5sum": "552b0ff0492bdab6cf69c1bc2d679e0a"
}
}
},
"chat_bubbles": {
"description": "Adds whatever the players say above their character, includes a few other features too!",
"external_url": "",
"authors": [
{
"name": "DinoWattz",
"email": "",
"discord": ""
}
],
"versions": {
"1.0.0": {
"api_version": 9,
"commit_sha": "e67d891",
"released_on": "23-02-2026",
"md5sum": "307175b18b80671a7984862bcf09f1ce"
}
}
},
"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": {
"api_version": 9,
"commit_sha": "e67d891",
"released_on": "23-02-2026",
"md5sum": "63c665905dd4619d3f99ca8977f8fbf3"
}
}
},
"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": {
"api_version": 9,
"commit_sha": "e67d891",
"released_on": "23-02-2026",
"md5sum": "5de7a474c3c6b21e8ea1e1abff3c07a1"
}
}
} }
} }
} }

View file

@ -0,0 +1,388 @@
# 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

View file

@ -0,0 +1,599 @@
# 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",
)
# Prints chat messages to the console as "[Character's Name] Player 1/Player 2: Hello World!"
PRINT_CHAT_MESSAGES = True
BUBBLE_DURATION: float = 4.7 # 4.7 matches the default onscreen message duration
SPEAK_VOLUME = 1.5
SHOUT_VOLUME = 3.0
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)

View file

@ -0,0 +1,305 @@
# 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

View file

@ -0,0 +1,129 @@
# 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