diff --git a/plugins/utilities/bomb_radius_visualizer.py b/plugins/utilities/bomb_radius_visualizer.py new file mode 100644 index 0000000..cc5beb5 --- /dev/null +++ b/plugins/utilities/bomb_radius_visualizer.py @@ -0,0 +1,85 @@ +# ba_meta require api 7 + +""" + Bomb Radius Visualizer by TheMikirog + + With this cutting edge technology, you precisely know + how close to the bomb you can tread. Supports modified blast radius values! +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +# Let's import everything we need and nothing more. +import ba +import bastd +from bastd.actor.bomb import Bomb + +if TYPE_CHECKING: + pass + +# ba_meta export plugin +class BombRadiusVisualizer(ba.Plugin): + + # We use a decorator to add extra code to existing code, increasing mod compatibility. + # Here I'm defining a new bomb init function that'll be replaced. + def new_bomb_init(func): + # This function will return our wrapper function, which is going to take the original function's base arguments. + # Yes, in Python functions are objects that can be passed as arguments. It's bonkers. + # arg[0] is "self" in our original bomb init function. + # We're working kind of blindly here, so it's good to have the original function + # open in a second window for argument reference. + def wrapper(*args, **kwargs): + # Here's where we execute the original game's code, so it's not lost. + # We want to add our code at the end of the existing code, so our code goes under that. + func(*args, **kwargs) + + # Let's make a new node that's just a circle. It's the some one used in the Target Practice minigame. + # This is going to make a slightly opaque red circle, signifying damaging area. + # We aren't defining the size, because we're gonna animate it shortly after. + args[0].radius_visualizer = ba.newnode('locator', + owner=args[0].node, # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circle', + 'color': (1, 0, 0), + 'opacity':0.05, + 'draw_beauty': False, + 'additive': False + }) + # Let's connect our circle to the bomb. + args[0].node.connectattr('position', args[0].radius_visualizer, 'position') + + # Let's do a fancy animation of that red circle growing into shape like a cartoon. + # We're gonna read our bomb's blast radius and use it to decide the size of our circle. + ba.animate_array(args[0].radius_visualizer, 'size', 1, { + 0.0: [0.0], + 0.2: [args[0].blast_radius * 2.2], + 0.25: [args[0].blast_radius * 2.0] + }) + + # Let's do a second circle, this time just the outline to where the damaging area ends. + args[0].radius_visualizer_circle = ba.newnode('locator', + owner=args[0].node, # Remove itself when the bomb node dies. + attrs={ + 'shape': 'circleOutline', + 'size':[args[0].blast_radius * 2.0], # Here's that bomb's blast radius value again! + 'color': (1, 1, 0), + 'draw_beauty': False, + 'additive': True + }) + # Attach the circle to the bomb. + args[0].node.connectattr('position', args[0].radius_visualizer_circle, 'position') + + # Let's animate that circle too, but this time let's do the opacity. + ba.animate( + args[0].radius_visualizer_circle, 'opacity', { + 0: 0.0, + 0.4: 0.1 + }) + return wrapper + + # Finally we """travel through the game files""" to replace the function we want with our own version. + # We transplant the old function's arguments into our version. + bastd.actor.bomb.Bomb.__init__ = new_bomb_init(bastd.actor.bomb.Bomb.__init__) + \ No newline at end of file diff --git a/plugins/utilities/quickturn.py b/plugins/utilities/quickturn.py new file mode 100644 index 0000000..eff5fd5 --- /dev/null +++ b/plugins/utilities/quickturn.py @@ -0,0 +1,133 @@ +""" + Quickturn by TheMikirog + + Super Smash Bros Melee's wavedash mechanic ported and tuned for BombSquad. + + Sharp turns while running (releasing run button, changing direction, holding run again) are much faster with this mod, allowing for more dynamic, aggressive play. + Version 3 + +""" + +# ba_meta require api 7 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import math +import bastd +from bastd.actor.spaz import Spaz + +if TYPE_CHECKING: + pass + +# ba_meta export plugin +class Quickturn(ba.Plugin): + + class FootConnectMessage: + """Spaz started touching the ground""" + + class FootDisconnectMessage: + """Spaz stopped touching the ground""" + + def wavedash(self) -> None: + if not self.node: + return + + isMoving = abs(self.node.move_up_down) >= 0.5 or abs(self.node.move_left_right) >= 0.5 + + if self._dead or not self.grounded or not isMoving: + return + + if self.node.knockout > 0.0 or self.frozen or self.node.hold_node: + return + + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + + if t_ms - self.last_wavedash_time_ms >= self._wavedash_cooldown: + + move = [self.node.move_left_right, -self.node.move_up_down] + vel = [self.node.velocity[0], self.node.velocity[2]] + + move_length = math.hypot(move[0], move[1]) + vel_length = math.hypot(vel[0], vel[1]) + if vel_length < 0.6: return + move_norm = [m/move_length for m in move] + vel_norm = [v/vel_length for v in vel] + dot = sum(x*y for x,y in zip(move_norm,vel_norm)) + turn_power = min(round(math.acos(dot) / math.pi,2)*1.3,1) + + + # https://easings.net/#easeInOutQuart + if turn_power < 0.55: + turn_power = 8 * turn_power * turn_power * turn_power * turn_power + else: + turn_power = 0.55 - pow(-2 * turn_power + 2, 4) / 2 + if turn_power < 0.1: return + + boost_power = math.sqrt(math.pow(vel[0],2) + math.pow(vel[1],2)) * 8 + boost_power = min(pow(boost_power,8),160) + + self.last_wavedash_time_ms = t_ms + + # FX + ba.emitfx(position=self.node.position, + velocity=(vel[0]*0.5,-1,vel[1]*0.5), + chunk_type='spark', + count=5, + scale=boost_power / 160 * turn_power, + spread=0.25); + + # Boost itself + pos = self.node.position + for i in range(6): + self.node.handlemessage('impulse',pos[0],0.2+pos[1]+i*0.1,pos[2], + 0,0,0, + boost_power * turn_power, + boost_power * turn_power,0,0, + move[0],0,move[1]) + + def new_spaz_init(func): + def wrapper(*args, **kwargs): + + func(*args, **kwargs) + + # args[0] = self + args[0]._wavedash_cooldown = 170 + args[0].last_wavedash_time_ms = -9999 + args[0].grounded = 0 + + return wrapper + bastd.actor.spaz.Spaz.__init__ = new_spaz_init(bastd.actor.spaz.Spaz.__init__) + + def new_factory(func): + def wrapper(*args, **kwargs): + func(*args, **kwargs) + + args[0].roller_material.add_actions( + conditions=('they_have_material', bastd.gameutils.SharedObjects.get().footing_material), + actions=(('message', 'our_node', 'at_connect', Quickturn.FootConnectMessage), + ('message', 'our_node', 'at_disconnect', Quickturn.FootDisconnectMessage))) + return wrapper + bastd.actor.spazfactory.SpazFactory.__init__ = new_factory(bastd.actor.spazfactory.SpazFactory.__init__) + + def new_handlemessage(func): + def wrapper(*args, **kwargs): + if args[1] == Quickturn.FootConnectMessage: + args[0].grounded += 1 + elif args[1] == Quickturn.FootDisconnectMessage: + if args[0].grounded > 0: args[0].grounded -= 1 + + func(*args, **kwargs) + return wrapper + bastd.actor.spaz.Spaz.handlemessage = new_handlemessage(bastd.actor.spaz.Spaz.handlemessage) + + def new_on_run(func): + def wrapper(*args, **kwargs): + if args[0]._last_run_value < args[1] and args[1] > 0.8: + Quickturn.wavedash(args[0]) + func(*args, **kwargs) + return wrapper + bastd.actor.spaz.Spaz.on_run = new_on_run(bastd.actor.spaz.Spaz.on_run) \ No newline at end of file diff --git a/plugins/utilities/ragdoll-b-gone.py b/plugins/utilities/ragdoll-b-gone.py new file mode 100644 index 0000000..86bb44d --- /dev/null +++ b/plugins/utilities/ragdoll-b-gone.py @@ -0,0 +1,110 @@ +# ba_meta require api 7 + +""" + Ragdoll-B-Gone by TheMikirog + + Removes ragdolls. + Thanos snaps those pesky feet-tripping body sacks out of existence. + Literally that's it. + + Heavily commented for easy modding learning! + +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +# Let's import everything we need and nothing more. +import ba +import bastd +import random +from bastd.actor.spaz import Spaz +from bastd.actor.spazfactory import SpazFactory + +if TYPE_CHECKING: + pass + +# ba_meta export plugin +class RagdollBGone(ba.Plugin): + + # We use a decorator to add extra code to existing code, increasing mod compatibility. + # Any gameplay altering mod should master the decorator! + # Here I'm defining a new handlemessage function that'll be replaced. + def new_handlemessage(func): + # This function will return our wrapper function, which is going to take the original function's base arguments. + # Yes, in Python functions are objects that can be passed as arguments. It's bonkers. + # arg[0] is "self", args[1] is "msg" in our original handlemessage function. + # We're working kind of blindly here, so it's good to have the original function + # open in a second window for argument reference. + def wrapper(*args, **kwargs): + if isinstance(args[1], ba.DieMessage): # Replace Spaz death behavior + + # Here we play the gamey death noise in Co-op. + if not args[1].immediate: + if args[0].play_big_death_sound and not args[0]._dead: + ba.playsound(SpazFactory.get().single_player_death_sound) + + # If our Spaz dies by falling out of the map, we want to keep the ragdoll. + # Ragdolls don't impact gameplay if Spaz dies this way, so it's fine if we leave the behavior as is. + if args[1].how == ba.DeathType.FALL: + # The next two properties are all built-in, so their behavior can't be edited directly without touching the C++ layer. + # We can change their values though! + # "hurt" property is basically the health bar above the player and the blinking when low on health. + # 1.0 means empty health bar and the fastest blinking in the west. + args[0].node.hurt = 1.0 + # Make our Spaz close their eyes permanently and then make their body disintegrate. + # Again, this behavior is built in. We can only trigger it by setting "dead" to True. + args[0].node.dead = True + # After the death animation ends (which is around 2 seconds) let's remove the Spaz our of existence. + ba.timer(2.0, args[0].node.delete) + else: + # Here's our new behavior! + # The idea is to remove the Spaz node and make some sparks for extra flair. + # First we see if that node even exists, just in case. + if args[0].node: + # Make sure Spaz isn't dead, so we can perform the removal. + if not args[0]._dead: + # Run this next piece of code 4 times. + # "i" will start at 0 and becomes higher each iteration until it reaches 3. + for i in range(4): + # XYZ position of our sparks, we'll take the Spaz position as a base. + pos = (args[0].node.position[0], + # Let's spread the sparks across the body, assuming Spaz is standing straight. + # We're gonna change the Y axis position, which is height. + args[0].node.position[1] + i * 0.2, + args[0].node.position[2]) + # This function allows us to spawn particles like sparks and bomb shrapnel. + # We're gonna use sparks here. + ba.emitfx(position = pos, # Here we place our edited position. + velocity=args[0].node.velocity, + count=random.randrange(2, 5), # Random amount of sparks between 2 and 5 + scale=3.0, + spread=0.2, + chunk_type='spark') + + # Make a Spaz death noise if we're not gibbed. + if not args[0].shattered: + death_sounds = args[0].node.death_sounds # Get our Spaz's death noises, these change depending on character skins + sound = death_sounds[random.randrange(len(death_sounds))] # Pick a random death noise + ba.playsound(sound, position=args[0].node.position) # Play the sound where our Spaz is + # Delete our Spaz node immediately. + # Removing stuff is weird and prone to errors, so we're gonna delay it. + ba.timer(0.001, args[0].node.delete) + + # Let's mark our Spaz as dead, so he can't die again. + # Notice how we're targeting the Spaz and not it's node. + # "self.node" is a visual representation of the character while "self" is his game logic. + args[0]._dead = True + args[0].hitpoints = 0 # Set his health to zero. This value is independent from the health bar above his head. + return + + # Worry no longer! We're not gonna remove all the base game code! + # Here's where we bring it all back. + # If I wanted to add extra code at the end of the base game's behavior, I would just put that at the beginning of my function. + func(*args, **kwargs) + return wrapper + + # Finally we """travel through the game files""" to replace the function we want with our own version. + # We transplant the old function's arguments into our version. + bastd.actor.spaz.Spaz.handlemessage = new_handlemessage(bastd.actor.spaz.Spaz.handlemessage)