[ci] auto-format

This commit is contained in:
TheMikirog 2022-11-13 20:06:02 +00:00 committed by github-actions[bot]
parent 02cb7871ee
commit 05d2f1a4f7
4 changed files with 340 additions and 304 deletions

View file

@ -32,24 +32,24 @@ import random
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
# Let's define stun times for falling. # Let's define stun times for falling.
# First element is stun for the first fall, second element is stun for the second fall and so on. # First element is stun for the first fall, second element is stun for the second fall and so on.
# If we fall more than the amount of elements on this list, we'll use the last entry. # If we fall more than the amount of elements on this list, we'll use the last entry.
FALL_PENALTIES = [1.5, FALL_PENALTIES = [1.5,
2.5, 2.5,
3.5, 3.5,
5.0, 5.0,
6.0, 6.0,
7.0, 7.0,
8.0, 8.0,
9.0, 9.0,
10.0] 10.0]
RED_COLOR = (1.0, 0.2, 0.2) RED_COLOR = (1.0, 0.2, 0.2)
YELLOW_COLOR = (1.0, 1.0, 0.2) YELLOW_COLOR = (1.0, 1.0, 0.2)
# The player in Hot Potato can be in one of these states: # The player in Hot Potato can be in one of these states:
class PlayerState(Enum): class PlayerState(Enum):
# REGULAR - the state all players start in. # REGULAR - the state all players start in.
@ -70,6 +70,8 @@ class PlayerState(Enum):
# To make the game easier to parse, I added Elimination style icons to the bottom of the screen. # To make the game easier to parse, I added Elimination style icons to the bottom of the screen.
# Here's the behavior of each icon. # Here's the behavior of each icon.
class Icon(ba.Actor): class Icon(ba.Actor):
"""Creates in in-game icon on screen.""" """Creates in in-game icon on screen."""
@ -85,7 +87,7 @@ class Icon(ba.Actor):
# Define the player this icon belongs to # Define the player this icon belongs to
self._player = player self._player = player
self._name_scale = name_scale self._name_scale = name_scale
self._outline_tex = ba.gettexture('characterIconMask') self._outline_tex = ba.gettexture('characterIconMask')
# Character portrait # Character portrait
@ -153,7 +155,7 @@ class Icon(ba.Actor):
}) })
self.set_marked_icon(player.state) self.set_marked_icon(player.state)
self.set_position_and_scale(position, scale) self.set_position_and_scale(position, scale)
# Change our icon's appearance depending on the player state. # Change our icon's appearance depending on the player state.
def set_marked_icon(self, type: PlayerState) -> None: def set_marked_icon(self, type: PlayerState) -> None:
pos = self.node.position pos = self.node.position
@ -191,16 +193,16 @@ class Icon(ba.Actor):
self._marked_icon.position = (pos[0] - 2, pos[1] - 12) self._marked_icon.position = (pos[0] - 2, pos[1] - 12)
self._marked_text.text = 'You\'re Out!' self._marked_text.text = 'You\'re Out!'
self._marked_text.color = (0.5, 0.5, 0.5) self._marked_text.color = (0.5, 0.5, 0.5)
# Animate text and icon # Animate text and icon
animation_end_time = 1.5 if bool(self.activity.settings['Epic Mode']) else 3.0 animation_end_time = 1.5 if bool(self.activity.settings['Epic Mode']) else 3.0
ba.animate(self._marked_icon,'opacity', { ba.animate(self._marked_icon, 'opacity', {
0: 1.0, 0: 1.0,
animation_end_time: 0.0}) animation_end_time: 0.0})
ba.animate(self._marked_text,'opacity', { ba.animate(self._marked_text, 'opacity', {
0: 1.0, 0: 1.0,
animation_end_time: 0.0}) animation_end_time: 0.0})
self._name_text.opacity = 0.2 self._name_text.opacity = 0.2
assert self.node assert self.node
self.node.color = (0.7, 0.3, 0.3) self.node.color = (0.7, 0.3, 0.3)
@ -223,50 +225,52 @@ class Icon(ba.Actor):
# This gamemode heavily relies on edited player behavior. # This gamemode heavily relies on edited player behavior.
# We need that amount of control, so we're gonna create our own class and use the original PlayerSpaz as our blueprint. # We need that amount of control, so we're gonna create our own class and use the original PlayerSpaz as our blueprint.
class PotatoPlayerSpaz(PlayerSpaz): class PotatoPlayerSpaz(PlayerSpaz):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # unchanged Spaz __init__ code goes here super().__init__(*args, **kwargs) # unchanged Spaz __init__ code goes here
self.dropped_bombs = [] # we use this to track bombs thrown by the player self.dropped_bombs = [] # we use this to track bombs thrown by the player
# Define a marked light # Define a marked light
self.marked_light = ba.newnode('light', self.marked_light = ba.newnode('light',
owner=self.node, owner=self.node,
attrs={'position':self.node.position, attrs={'position': self.node.position,
'radius':0.15, 'radius': 0.15,
'intensity':0.0, 'intensity': 0.0,
'height_attenuated':False, 'height_attenuated': False,
'color': (1.0, 0.0, 0.0)}) 'color': (1.0, 0.0, 0.0)})
# Pulsing red light when the player is Marked # Pulsing red light when the player is Marked
ba.animate(self.marked_light,'radius',{ ba.animate(self.marked_light, 'radius', {
0: 0.1, 0: 0.1,
0.3: 0.15, 0.3: 0.15,
0.6: 0.1}, 0.6: 0.1},
loop = True) loop=True)
self.node.connectattr('position_center',self.marked_light,'position') self.node.connectattr('position_center', self.marked_light, 'position')
# Marked timer. It should be above our head, so we attach the text to the offset that's attached to the player. # Marked timer. It should be above our head, so we attach the text to the offset that's attached to the player.
self.marked_timer_offset = ba.newnode('math', owner = self.node, attrs = { self.marked_timer_offset = ba.newnode('math', owner=self.node, attrs={
'input1': (0, 1.2, 0), 'input1': (0, 1.2, 0),
'operation': 'add'}) 'operation': 'add'})
self.node.connectattr('torso_position', self.marked_timer_offset, 'input2') self.node.connectattr('torso_position', self.marked_timer_offset, 'input2')
self.marked_timer_text = ba.newnode('text', owner = self.node, attrs = { self.marked_timer_text = ba.newnode('text', owner=self.node, attrs={
'text': '', 'text': '',
'in_world': True, 'in_world': True,
'shadow': 0.4, 'shadow': 0.4,
'color': (RED_COLOR[0], RED_COLOR[1], RED_COLOR[2], 0.0), 'color': (RED_COLOR[0], RED_COLOR[1], RED_COLOR[2], 0.0),
'flatness': 0, 'flatness': 0,
'scale': 0.02, 'scale': 0.02,
'h_align': 'center'}) 'h_align': 'center'})
self.marked_timer_offset.connectattr('output', self.marked_timer_text, 'position') self.marked_timer_offset.connectattr('output', self.marked_timer_text, 'position')
# Modified behavior when dropping bombs # Modified behavior when dropping bombs
def drop_bomb(self) -> stdbomb.Bomb | None: def drop_bomb(self) -> stdbomb.Bomb | None:
# The original function returns the Bomb the player created. # The original function returns the Bomb the player created.
# This is super helpful for us, since all we need is to mark the bombs red # This is super helpful for us, since all we need is to mark the bombs red
# if they belong to the Marked player and nothing else. # if they belong to the Marked player and nothing else.
bomb = super().drop_bomb() bomb = super().drop_bomb()
# Let's make sure the player actually created a new bomb # Let's make sure the player actually created a new bomb
@ -275,33 +279,33 @@ class PotatoPlayerSpaz(PlayerSpaz):
self.dropped_bombs.append(bomb) self.dropped_bombs.append(bomb)
# Bring a light # Bring a light
bomb.bomb_marked_light = ba.newnode('light', bomb.bomb_marked_light = ba.newnode('light',
owner=bomb.node, owner=bomb.node,
attrs={'position':bomb.node.position, attrs={'position': bomb.node.position,
'radius':0.04, 'radius': 0.04,
'intensity':0.0, 'intensity': 0.0,
'height_attenuated':False, 'height_attenuated': False,
'color': (1.0, 0.0, 0.0)}) 'color': (1.0, 0.0, 0.0)})
# Attach the light to the bomb # Attach the light to the bomb
bomb.node.connectattr('position',bomb.bomb_marked_light,'position') bomb.node.connectattr('position', bomb.bomb_marked_light, 'position')
# Let's adjust all lights for all bombs that we own. # Let's adjust all lights for all bombs that we own.
self.set_bombs_marked() self.set_bombs_marked()
# When the bomb physics node dies, call a function. # When the bomb physics node dies, call a function.
bomb.node.add_death_action( bomb.node.add_death_action(
ba.WeakCall(self.bomb_died, bomb)) ba.WeakCall(self.bomb_died, bomb))
# Here's the function that gets called when one of the player's bombs dies. # Here's the function that gets called when one of the player's bombs dies.
# We reference the player's dropped_bombs list and remove the bomb that died. # We reference the player's dropped_bombs list and remove the bomb that died.
def bomb_died(self, bomb): def bomb_died(self, bomb):
self.dropped_bombs.remove(bomb) self.dropped_bombs.remove(bomb)
# Go through all the bombs this player has in the world. # Go through all the bombs this player has in the world.
# Paint them red if the owner is marked, turn off the light otherwise. # Paint them red if the owner is marked, turn off the light otherwise.
# We need this light to inform the player about bombs YOU DON'T want to get hit by. # We need this light to inform the player about bombs YOU DON'T want to get hit by.
def set_bombs_marked(self): def set_bombs_marked(self):
for bomb in self.dropped_bombs: for bomb in self.dropped_bombs:
bomb.bomb_marked_light.intensity = 20.0 if self._player.state == PlayerState.MARKED else 0.0 bomb.bomb_marked_light.intensity = 20.0 if self._player.state == PlayerState.MARKED else 0.0
# Since our gamemode relies heavily on players passing the mark to other players # Since our gamemode relies heavily on players passing the mark to other players
# we need to have access to this message. This gets called when the player takes damage for any reason. # we need to have access to this message. This gets called when the player takes damage for any reason.
def handlemessage(self, msg): def handlemessage(self, msg):
@ -312,12 +316,13 @@ class PotatoPlayerSpaz(PlayerSpaz):
# I'm still gonna comment all of it since we're here. # I'm still gonna comment all of it since we're here.
if not self.node: if not self.node:
return None return None
# If the attacker is marked, pass that mark to us. # If the attacker is marked, pass that mark to us.
self.activity.pass_mark(msg._source_player, self._player) self.activity.pass_mark(msg._source_player, self._player)
# When stun timer runs out, we explode. Let's make sure our own explosion does throw us around. # When stun timer runs out, we explode. Let's make sure our own explosion does throw us around.
if msg.hit_type == 'stun_blast' and msg._source_player == self.source_player: return True if msg.hit_type == 'stun_blast' and msg._source_player == self.source_player:
return True
# If the attacker is healthy and we're stunned, do a flash and play a sound, then ignore the rest of the code. # If the attacker is healthy and we're stunned, do a flash and play a sound, then ignore the rest of the code.
if self.source_player.state == PlayerState.STUNNED and msg._source_player != PlayerState.MARKED: if self.source_player.state == PlayerState.STUNNED and msg._source_player != PlayerState.MARKED:
self.node.handlemessage('flash') self.node.handlemessage('flash')
@ -325,7 +330,7 @@ class PotatoPlayerSpaz(PlayerSpaz):
1.0, 1.0,
position=self.node.position) position=self.node.position)
return True return True
# Here's all the damage and force calculations unchanged from the source. # Here's all the damage and force calculations unchanged from the source.
mag = msg.magnitude * self.impact_scale mag = msg.magnitude * self.impact_scale
velocity_mag = msg.velocity_magnitude * self.impact_scale velocity_mag = msg.velocity_magnitude * self.impact_scale
@ -341,8 +346,8 @@ class PotatoPlayerSpaz(PlayerSpaz):
velocity_mag, msg.radius, 0, msg.force_direction[0], velocity_mag, msg.radius, 0, msg.force_direction[0],
msg.force_direction[1], msg.force_direction[2]) msg.force_direction[1], msg.force_direction[2])
damage = int(damage_scale * self.node.damage) damage = int(damage_scale * self.node.damage)
self.node.handlemessage('hurt_sound') # That's how we play spaz node's hurt sound self.node.handlemessage('hurt_sound') # That's how we play spaz node's hurt sound
# Play punch impact sounds based on damage if it was a punch. # Play punch impact sounds based on damage if it was a punch.
# We don't show damage percentages, because it's irrelevant. # We don't show damage percentages, because it's irrelevant.
if msg.hit_type == 'punch': if msg.hit_type == 'punch':
@ -411,7 +416,7 @@ class PotatoPlayerSpaz(PlayerSpaz):
count=min(10, 1 + int(damage * 0.01)), count=min(10, 1 + int(damage * 0.01)),
scale=0.4, scale=0.4,
spread=0.1) spread=0.1)
# Briefly flash when hit. # Briefly flash when hit.
# We shouldn't do this if we're dead. # We shouldn't do this if we're dead.
if self.hitpoints > 0: if self.hitpoints > 0:
@ -427,14 +432,14 @@ class PotatoPlayerSpaz(PlayerSpaz):
# Make sure our body exists. # Make sure our body exists.
if not self.node: if not self.node:
return None return None
# Let's get all collision data if we can. Otherwise cancel. # Let's get all collision data if we can. Otherwise cancel.
try: try:
collision = ba.getcollision() collision = ba.getcollision()
opposingnode = collision.opposingnode opposingnode = collision.opposingnode
except ba.NotFoundError: except ba.NotFoundError:
return True return True
# Our grabber needs to be a Spaz # Our grabber needs to be a Spaz
if opposingnode.getnodetype() == 'spaz': if opposingnode.getnodetype() == 'spaz':
# Disallow grabbing if a healthy player tries to grab us and we're stunned. # Disallow grabbing if a healthy player tries to grab us and we're stunned.
@ -449,32 +454,35 @@ class PotatoPlayerSpaz(PlayerSpaz):
# If they're marked and we're healthy or stunned, pass that mark along to us. # If they're marked and we're healthy or stunned, pass that mark along to us.
elif opposingnode.source_player.state in [PlayerState.REGULAR, PlayerState.STUNNED] and self.source_player.state == PlayerState.MARKED: elif opposingnode.source_player.state in [PlayerState.REGULAR, PlayerState.STUNNED] and self.source_player.state == PlayerState.MARKED:
self.activity.pass_mark(self.source_player, opposingnode.source_player) self.activity.pass_mark(self.source_player, opposingnode.source_player)
# Our work is done. Continue with the rest of the grabbing behavior as usual. # Our work is done. Continue with the rest of the grabbing behavior as usual.
super().handlemessage(msg) super().handlemessage(msg)
# Dying is important in this gamemode and as such we need to address this behavior. # Dying is important in this gamemode and as such we need to address this behavior.
elif isinstance(msg, ba.DieMessage): elif isinstance(msg, ba.DieMessage):
# If a player left the game, inform our gamemode logic. # If a player left the game, inform our gamemode logic.
if msg.how == ba.DeathType.LEFT_GAME: if msg.how == ba.DeathType.LEFT_GAME:
self.activity.player_left(self.source_player) self.activity.player_left(self.source_player)
# If a MARKED or STUNNED player dies, hide the text from the previous spaz. # If a MARKED or STUNNED player dies, hide the text from the previous spaz.
if self.source_player.state in [PlayerState.MARKED, PlayerState.STUNNED]: if self.source_player.state in [PlayerState.MARKED, PlayerState.STUNNED]:
self.marked_timer_text.color = (self.marked_timer_text.color[0], self.marked_timer_text.color = (self.marked_timer_text.color[0],
self.marked_timer_text.color[1], self.marked_timer_text.color[1],
self.marked_timer_text.color[2], self.marked_timer_text.color[2],
0.0) 0.0)
ba.animate(self.marked_light,'intensity',{ ba.animate(self.marked_light, 'intensity', {
0: self.marked_light.intensity, 0: self.marked_light.intensity,
0.5: 0.0}) 0.5: 0.0})
# Continue with the rest of the behavior. # Continue with the rest of the behavior.
super().handlemessage(msg) super().handlemessage(msg)
# If a message is something we haven't modified yet, let's pass it along to the original. # If a message is something we haven't modified yet, let's pass it along to the original.
else: super().handlemessage(msg) else:
super().handlemessage(msg)
# A concept of a player is very useful to reference if we don't have a player character present (maybe they died). # A concept of a player is very useful to reference if we don't have a player character present (maybe they died).
class Player(ba.Player['Team']): class Player(ba.Player['Team']):
"""Our player type for this game.""" """Our player type for this game."""
@ -487,61 +495,66 @@ class Player(ba.Player['Team']):
# These are references to timers responsible for handling stunned behavior. # These are references to timers responsible for handling stunned behavior.
self.stunned_timer = None self.stunned_timer = None
self.stunned_update_timer = None self.stunned_update_timer = None
# If we're stunned, a timer calls this every 0.1 seconds. # If we're stunned, a timer calls this every 0.1 seconds.
def stunned_timer_tick(self) -> None: def stunned_timer_tick(self) -> None:
# Decrease our time remaining then change the text displayed above the Spaz's head # Decrease our time remaining then change the text displayed above the Spaz's head
self.stunned_time_remaining -= 0.1 self.stunned_time_remaining -= 0.1
self.stunned_time_remaining = max(0.0, self.stunned_time_remaining) self.stunned_time_remaining = max(0.0, self.stunned_time_remaining)
self.actor.marked_timer_text.text = str(round(self.stunned_time_remaining, 2)) self.actor.marked_timer_text.text = str(round(self.stunned_time_remaining, 2))
# When stun time is up, call this function. # When stun time is up, call this function.
def stun_remove(self) -> None: def stun_remove(self) -> None:
# Let's proceed only if we're stunned # Let's proceed only if we're stunned
if self.state != PlayerState.STUNNED: return if self.state != PlayerState.STUNNED:
# Do an explosion where we're standing. Normally it would throw us around, but we dealt return
# Do an explosion where we're standing. Normally it would throw us around, but we dealt
# with this issue in PlayerSpaz's edited HitMessage in line 312. # with this issue in PlayerSpaz's edited HitMessage in line 312.
Blast(position=self.actor.node.position, Blast(position=self.actor.node.position,
velocity=self.actor.node.velocity, velocity=self.actor.node.velocity,
blast_radius=2.5, blast_radius=2.5,
hit_type='stun_blast', # This hit type allows us to ignore our own stun blast explosions. # This hit type allows us to ignore our own stun blast explosions.
hit_type='stun_blast',
source_player=self).autoretain() source_player=self).autoretain()
# Let's switch our state back to healthy. # Let's switch our state back to healthy.
self.set_state(PlayerState.REGULAR) self.set_state(PlayerState.REGULAR)
# States are a key part of this gamemode and a lot of logic has to be done to acknowledge these state changes. # States are a key part of this gamemode and a lot of logic has to be done to acknowledge these state changes.
def set_state(self, state: PlayerState) -> None: def set_state(self, state: PlayerState) -> None:
# Let's remember our old state before we change it. # Let's remember our old state before we change it.
old_state = self.state old_state = self.state
# If we just became stunned, do all of this: # If we just became stunned, do all of this:
if old_state != PlayerState.STUNNED and state == PlayerState.STUNNED: if old_state != PlayerState.STUNNED and state == PlayerState.STUNNED:
self.actor.disconnect_controls_from_player() # Disallow all movement and actions self.actor.disconnect_controls_from_player() # Disallow all movement and actions
# Let's set our stun time based on the amount of times we fell out of the map. # Let's set our stun time based on the amount of times we fell out of the map.
if self.fall_times < len(FALL_PENALTIES): if self.fall_times < len(FALL_PENALTIES):
stun_time = FALL_PENALTIES[self.fall_times] stun_time = FALL_PENALTIES[self.fall_times]
else: else:
stun_time = FALL_PENALTIES[len(FALL_PENALTIES) - 1] stun_time = FALL_PENALTIES[len(FALL_PENALTIES) - 1]
self.stunned_time_remaining = stun_time # Set our stun time remaining self.stunned_time_remaining = stun_time # Set our stun time remaining
self.stunned_timer = ba.Timer(stun_time + 0.1, ba.Call(self.stun_remove)) # Remove our stun once the time is up # Remove our stun once the time is up
self.stunned_update_timer = ba.Timer(0.1, ba.Call(self.stunned_timer_tick), repeat = True) # Call a function every 0.1 seconds self.stunned_timer = ba.Timer(stun_time + 0.1, ba.Call(self.stun_remove))
self.fall_times += 1 # Increase the amount of times we fell by one self.stunned_update_timer = ba.Timer(0.1, ba.Call(
self.actor.marked_timer_text.text = str(stun_time) # Change the text above the Spaz's head to total stun time self.stunned_timer_tick), repeat=True) # Call a function every 0.1 seconds
self.fall_times += 1 # Increase the amount of times we fell by one
# Change the text above the Spaz's head to total stun time
self.actor.marked_timer_text.text = str(stun_time)
# If we were stunned, but now we're not, let's reconnect our controls. # If we were stunned, but now we're not, let's reconnect our controls.
# CODING CHALLENGE: to punch or bomb immediately after the stun ends, you need to # CODING CHALLENGE: to punch or bomb immediately after the stun ends, you need to
# time the button press frame-perfectly in order for it to work. # time the button press frame-perfectly in order for it to work.
# What if we could press the button shortly before stun ends to do the action as soon as possible? # What if we could press the button shortly before stun ends to do the action as soon as possible?
# If you're feeling up to the challenge, feel free to implement that! # If you're feeling up to the challenge, feel free to implement that!
if old_state == PlayerState.STUNNED and state != PlayerState.STUNNED: if old_state == PlayerState.STUNNED and state != PlayerState.STUNNED:
self.actor.connect_controls_to_player() self.actor.connect_controls_to_player()
# When setting a state that is not STUNNED, clear all timers. # When setting a state that is not STUNNED, clear all timers.
if state != PlayerState.STUNNED: if state != PlayerState.STUNNED:
self.stunned_timer = None self.stunned_timer = None
self.stunned_update_timer = None self.stunned_update_timer = None
# Here's all the light and text colors that we set depending on the state. # Here's all the light and text colors that we set depending on the state.
if state == PlayerState.MARKED: if state == PlayerState.MARKED:
self.actor.marked_light.intensity = 1.5 self.actor.marked_light.intensity = 1.5
@ -560,11 +573,11 @@ class Player(ba.Player['Team']):
else: else:
self.actor.marked_light.intensity = 0.0 self.actor.marked_light.intensity = 0.0
self.actor.marked_timer_text.text = '' self.actor.marked_timer_text.text = ''
self.state = state self.state = state
self.actor.set_bombs_marked() # Light our bombs red if we're Marked, removes the light otherwise self.actor.set_bombs_marked() # Light our bombs red if we're Marked, removes the light otherwise
self.icon.set_marked_icon(state) # Update our icon self.icon.set_marked_icon(state) # Update our icon
# ba_meta export game # ba_meta export game
class HotPotato(ba.TeamGameActivity[Player, ba.Team]): class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
@ -588,28 +601,28 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
'Red bombs belong to the Marked player!\nWatch out for those!', 'Red bombs belong to the Marked player!\nWatch out for those!',
'Stunned players explode when their stun timer runs out.\nIf that time is close to zero, keep your distance!' 'Stunned players explode when their stun timer runs out.\nIf that time is close to zero, keep your distance!'
] ]
# We're gonna distribute end of match session scores based on who dies first and who survives. # We're gonna distribute end of match session scores based on who dies first and who survives.
# First place gets most points, then second, then third. # First place gets most points, then second, then third.
scoreconfig = ba.ScoreConfig(label='Place', scoreconfig = ba.ScoreConfig(label='Place',
scoretype=ba.ScoreType.POINTS, scoretype=ba.ScoreType.POINTS,
lower_is_better=True) lower_is_better=True)
# These variables are self explanatory too. # These variables are self explanatory too.
show_kill_points = False show_kill_points = False
allow_mid_activity_joins = False allow_mid_activity_joins = False
# Let's define some settings the user can mess around with to fit their needs. # Let's define some settings the user can mess around with to fit their needs.
available_settings = [ available_settings = [
ba.IntSetting('Elimination Timer', ba.IntSetting('Elimination Timer',
min_value=5, min_value=5,
default=15, default=15,
increment=1, increment=1,
), ),
ba.BoolSetting('Marked Players use Impact Bombs', default=False), ba.BoolSetting('Marked Players use Impact Bombs', default=False),
ba.BoolSetting('Epic Mode', default=False), ba.BoolSetting('Epic Mode', default=False),
] ]
# Hot Potato is strictly a Free-For-All gamemode, so only picking the gamemode in FFA playlists. # Hot Potato is strictly a Free-For-All gamemode, so only picking the gamemode in FFA playlists.
@classmethod @classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
@ -620,12 +633,12 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
@classmethod @classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('melee') return ba.getmaps('melee')
# Here we define everything the gamemode needs, like sounds and settings. # Here we define everything the gamemode needs, like sounds and settings.
def __init__(self, settings: dict): def __init__(self, settings: dict):
super().__init__(settings) super().__init__(settings)
self.settings = settings self.settings = settings
# Let's define all of the sounds we need. # Let's define all of the sounds we need.
self._tick_sound = ba.getsound('tick') self._tick_sound = ba.getsound('tick')
self._player_eliminated_sound = ba.getsound('playerDeath') self._player_eliminated_sound = ba.getsound('playerDeath')
@ -637,13 +650,13 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
self._marked_sounds = [ba.getsound('powerdown01'), self._marked_sounds = [ba.getsound('powerdown01'),
ba.getsound('activateBeep'), ba.getsound('activateBeep'),
ba.getsound('hiss')] ba.getsound('hiss')]
# Normally play KOTH music, but switch to Epic music if we're in slow motion. # Normally play KOTH music, but switch to Epic music if we're in slow motion.
self._epic_mode = bool(settings['Epic Mode']) self._epic_mode = bool(settings['Epic Mode'])
self.slow_motion = self._epic_mode self.slow_motion = self._epic_mode
self.default_music = (ba.MusicType.EPIC if self._epic_mode else self.default_music = (ba.MusicType.EPIC if self._epic_mode else
ba.MusicType.SCARY) ba.MusicType.SCARY)
# This description appears below the title card after it comes crashing when the game begins. # This description appears below the title card after it comes crashing when the game begins.
def get_instance_description(self) -> str | Sequence: def get_instance_description(self) -> str | Sequence:
return 'Pass the mark to someone else before you explode!' return 'Pass the mark to someone else before you explode!'
@ -651,11 +664,11 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
# This is the tiny text that is displayed in the corner during the game as a quick reminder of the objective. # This is the tiny text that is displayed in the corner during the game as a quick reminder of the objective.
def get_instance_description_short(self) -> str | Sequence: def get_instance_description_short(self) -> str | Sequence:
return 'pass the mark' return 'pass the mark'
# Set up our player every time they join. # Set up our player every time they join.
# Because you can't join mid-match, this will always be called at the beginning of the game. # Because you can't join mid-match, this will always be called at the beginning of the game.
def on_player_join(self, player: Player) -> None: def on_player_join(self, player: Player) -> None:
player.state = PlayerState.REGULAR player.state = PlayerState.REGULAR
player.fall_times = 0 player.fall_times = 0
@ -663,7 +676,7 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
if not self.has_begun(): if not self.has_begun():
player.icon = Icon(player, position=(0, 50), scale=0.8) player.icon = Icon(player, position=(0, 50), scale=0.8)
self.spawn_player(player) self.spawn_player(player)
# Returns every single marked player. # Returns every single marked player.
# This piece of info is used excensively in this gamemode, so it's advantageous to have a function to cut on # This piece of info is used excensively in this gamemode, so it's advantageous to have a function to cut on
# work and make the gamemode easier to maintain # work and make the gamemode easier to maintain
@ -673,76 +686,77 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
if p.state == PlayerState.MARKED: if p.state == PlayerState.MARKED:
marked_players.append(p) marked_players.append(p)
return marked_players return marked_players
# Marks a player. This sets their state, spawns some particles and sets the timer text above their heads. # Marks a player. This sets their state, spawns some particles and sets the timer text above their heads.
def mark(self, target: Player) -> None: def mark(self, target: Player) -> None:
target.set_state(PlayerState.MARKED) target.set_state(PlayerState.MARKED)
ba.emitfx(position=target.actor.node.position, ba.emitfx(position=target.actor.node.position,
velocity=target.actor.node.velocity, velocity=target.actor.node.velocity,
chunk_type='spark', chunk_type='spark',
count=int(20.0+random.random()*20), count=int(20.0+random.random()*20),
scale=1.0, scale=1.0,
spread=1.0); spread=1.0)
if bool(self.settings['Marked Players use Impact Bombs']): if bool(self.settings['Marked Players use Impact Bombs']):
target.actor.bomb_type = 'impact' target.actor.bomb_type = 'impact'
target.actor.marked_timer_text.text = str(self.elimination_timer_display) target.actor.marked_timer_text.text = str(self.elimination_timer_display)
# Removes the mark from the player. This restores the player to its initial state. # Removes the mark from the player. This restores the player to its initial state.
def remove_mark(self, target: Player) -> None: def remove_mark(self, target: Player) -> None:
if target.state != PlayerState.MARKED: if target.state != PlayerState.MARKED:
return return
target.actor.bomb_type = 'normal' target.actor.bomb_type = 'normal'
target.set_state(PlayerState.REGULAR) target.set_state(PlayerState.REGULAR)
target.actor.marked_timer_text.text = '' target.actor.marked_timer_text.text = ''
# Pass the mark from one player to another. # Pass the mark from one player to another.
# This is more desirable than calling mark and remove_mark functions constantly and gives us # This is more desirable than calling mark and remove_mark functions constantly and gives us
# more control over the mark spreading mechanic. # more control over the mark spreading mechanic.
def pass_mark(self, marked_player: Player, hit_player: Player) -> None: def pass_mark(self, marked_player: Player, hit_player: Player) -> None:
# Make sure both players meet the requirements # Make sure both players meet the requirements
if not marked_player or not hit_player: return if not marked_player or not hit_player:
return
if marked_player.state == PlayerState.MARKED and hit_player.state != PlayerState.MARKED: if marked_player.state == PlayerState.MARKED and hit_player.state != PlayerState.MARKED:
self.mark(hit_player) self.mark(hit_player)
self.remove_mark(marked_player) self.remove_mark(marked_player)
# This function is called every second a marked player exists. # This function is called every second a marked player exists.
def _eliminate_tick(self) -> None: def _eliminate_tick(self) -> None:
marked_players = self.get_marked_players() marked_players = self.get_marked_players()
marked_player_amount = len(marked_players) marked_player_amount = len(marked_players)
# If there is no marked players, raise an exception. # If there is no marked players, raise an exception.
# This is used for debugging purposes, which lets us know we messed up somewhere else in the code. # This is used for debugging purposes, which lets us know we messed up somewhere else in the code.
if len(self.get_marked_players()) == 0: if len(self.get_marked_players()) == 0:
raise Exception("no marked players!") raise Exception("no marked players!")
self.elimination_timer_display -= 1 # Decrease our timer by one second. self.elimination_timer_display -= 1 # Decrease our timer by one second.
if self.elimination_timer_display > 1: if self.elimination_timer_display > 1:
sound_volume = 1.0 / marked_player_amount sound_volume = 1.0 / marked_player_amount
for target in marked_players: for target in marked_players:
ba.playsound(self._tick_sound, sound_volume, target.actor.node.position) ba.playsound(self._tick_sound, sound_volume, target.actor.node.position)
target.actor.marked_timer_text.text = str(self.elimination_timer_display) target.actor.marked_timer_text.text = str(self.elimination_timer_display)
# When counting down 3, 2, 1 play some dramatic sounds # When counting down 3, 2, 1 play some dramatic sounds
if self.elimination_timer_display <= 3: if self.elimination_timer_display <= 3:
# We store our dramatic sounds in an array, so we target a specific element on the array # We store our dramatic sounds in an array, so we target a specific element on the array
# depending on time remaining. Arrays start at index 0, so we need to decrease # depending on time remaining. Arrays start at index 0, so we need to decrease
# our variable by 1 to get the element index. # our variable by 1 to get the element index.
ba.playsound(self._danger_tick_sounds[self.elimination_timer_display - 1], 1.5) ba.playsound(self._danger_tick_sounds[self.elimination_timer_display - 1], 1.5)
else: else:
# Elimination timer is up! Let's eliminate all marked players. # Elimination timer is up! Let's eliminate all marked players.
self._eliminate_marked_players() self._eliminate_marked_players()
# This function explodes all marked players # This function explodes all marked players
def _eliminate_marked_players(self) -> None: def _eliminate_marked_players(self) -> None:
self.marked_tick_timer = None self.marked_tick_timer = None
for target in self.get_marked_players(): for target in self.get_marked_players():
target.set_state(PlayerState.ELIMINATED) target.set_state(PlayerState.ELIMINATED)
target.actor.marked_timer_text.text = '' target.actor.marked_timer_text.text = ''
Blast(position=target.actor.node.position, Blast(position=target.actor.node.position,
velocity=target.actor.node.velocity, velocity=target.actor.node.velocity,
blast_radius=3.0, blast_radius=3.0,
@ -755,48 +769,50 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
chunk_type='spark') chunk_type='spark')
target.actor.handlemessage(ba.DieMessage(how='marked_elimination')) target.actor.handlemessage(ba.DieMessage(how='marked_elimination'))
target.actor.shatter(extreme=True) target.actor.shatter(extreme=True)
self.match_placement.append(target.team) self.match_placement.append(target.team)
ba.playsound(self._player_eliminated_sound, 1.0) ba.playsound(self._player_eliminated_sound, 1.0)
# Let the gamemode know a Marked # Let the gamemode know a Marked
self.marked_players_died() self.marked_players_died()
# This function should be called when a Marked player dies, like when timer runs out or they leave the game. # This function should be called when a Marked player dies, like when timer runs out or they leave the game.
def marked_players_died(self) -> bool: def marked_players_died(self) -> bool:
alive_players = self.get_alive_players() alive_players = self.get_alive_players()
# Is there only one player remaining? Or none at all? Let's end the gamemode # Is there only one player remaining? Or none at all? Let's end the gamemode
if len(alive_players) < 2: if len(alive_players) < 2:
if len(alive_players) == 1: if len(alive_players) == 1:
self.match_placement.append(alive_players[0].team) # Let's add our lone survivor to the match placement list. # Let's add our lone survivor to the match placement list.
self.match_placement.append(alive_players[0].team)
# Wait a while to let this sink in before we announce our victor. # Wait a while to let this sink in before we announce our victor.
self._end_game_timer = ba.Timer(1.25, ba.Call(self.end_game)) self._end_game_timer = ba.Timer(1.25, ba.Call(self.end_game))
else: else:
# There's still players remaining, so let's wait a while before marking a new player. # There's still players remaining, so let's wait a while before marking a new player.
self.new_mark_timer = ba.Timer(2.0 if self.slow_motion else 4.0, ba.Call(self.new_mark)) self.new_mark_timer = ba.Timer(2.0 if self.slow_motion else 4.0, ba.Call(self.new_mark))
# Another extensively used function that returns all alive players. # Another extensively used function that returns all alive players.
def get_alive_players(self) -> Sequence[ba.Player]: def get_alive_players(self) -> Sequence[ba.Player]:
alive_players = [] alive_players = []
for player in self.players: for player in self.players:
if player.state == PlayerState.ELIMINATED: continue # Ignore players who have been eliminated if player.state == PlayerState.ELIMINATED:
continue # Ignore players who have been eliminated
if player.is_alive(): if player.is_alive():
alive_players.append(player) alive_players.append(player)
return alive_players return alive_players
# This function is called every time we want to start a new "round" by marking a random player. # This function is called every time we want to start a new "round" by marking a random player.
def new_mark(self) -> None: def new_mark(self) -> None:
# Don't mark a new player if we've already announced a victor. # Don't mark a new player if we've already announced a victor.
if self.has_ended(): if self.has_ended():
return return
possible_targets = self.get_alive_players() possible_targets = self.get_alive_players()
all_victims = [] all_victims = []
# Let's mark TWO players at once if there's 6 or more players. Helps with the pacing. # Let's mark TWO players at once if there's 6 or more players. Helps with the pacing.
multi_choice = len(possible_targets) > 5 multi_choice = len(possible_targets) > 5
if multi_choice: if multi_choice:
# Pick our first victim at random. # Pick our first victim at random.
first_victim = random.choice(possible_targets) first_victim = random.choice(possible_targets)
@ -807,9 +823,11 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
else: else:
# Pick one victim at random. # Pick one victim at random.
all_victims = [random.choice(possible_targets)] all_victims = [random.choice(possible_targets)]
self.elimination_timer_display = self.settings['Elimination Timer'] # Set time until marked players explode # Set time until marked players explode
self.marked_tick_timer = ba.Timer(1.0, ba.Call(self._eliminate_tick), repeat=True) # Set a timer that calls _eliminate_tick every second self.elimination_timer_display = self.settings['Elimination Timer']
# Set a timer that calls _eliminate_tick every second
self.marked_tick_timer = ba.Timer(1.0, ba.Call(self._eliminate_tick), repeat=True)
# Mark all chosen victims and play a sound # Mark all chosen victims and play a sound
for new_victim in all_victims: for new_victim in all_victims:
# _marked_sounds is an array. # _marked_sounds is an array.
@ -818,14 +836,14 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
for sound in self._marked_sounds: for sound in self._marked_sounds:
ba.playsound(sound, 1.0, new_victim.actor.node.position) ba.playsound(sound, 1.0, new_victim.actor.node.position)
self.mark(new_victim) self.mark(new_victim)
# This function is called when the gamemode first loads. # This function is called when the gamemode first loads.
def on_begin(self) -> None: def on_begin(self) -> None:
super().on_begin() # Do standard gamemode on_begin behavior super().on_begin() # Do standard gamemode on_begin behavior
self.elimination_timer_display = 0 self.elimination_timer_display = 0
self.match_placement = [] self.match_placement = []
# End the game if there's only one player # End the game if there's only one player
if len(self.players) < 2: if len(self.players) < 2:
self.match_placement.append(self.players[0].team) self.match_placement.append(self.players[0].team)
@ -833,9 +851,9 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
else: else:
# Pick random player(s) to get marked # Pick random player(s) to get marked
self.new_mark_timer = ba.Timer(2.0 if self.slow_motion else 5.2, ba.Call(self.new_mark)) self.new_mark_timer = ba.Timer(2.0 if self.slow_motion else 5.2, ba.Call(self.new_mark))
self._update_icons() # Create player state icons self._update_icons() # Create player state icons
# This function creates and positions player state icons # This function creates and positions player state icons
def _update_icons(self): def _update_icons(self):
count = len(self.teams) count = len(self.teams)
@ -847,12 +865,12 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
player = team.players[0] player = team.players[0]
player.icon.set_position_and_scale((xval, 50), 0.8) player.icon.set_position_and_scale((xval, 50), 0.8)
xval += x_offs xval += x_offs
# Hot Potato can be a bit much, so I opted to show gameplay tips at the start of the match. # Hot Potato can be a bit much, so I opted to show gameplay tips at the start of the match.
# However because I put player state icons, the tips overlay the icons. # However because I put player state icons, the tips overlay the icons.
# I'm gonna modify this function to move the tip text above the icons. # I'm gonna modify this function to move the tip text above the icons.
def _show_tip(self) -> None: def _show_tip(self) -> None:
from ba._gameutils import animate, GameTip from ba._gameutils import animate, GameTip
from ba._generated.enums import SpecialChar from ba._generated.enums import SpecialChar
from ba._language import Lstr from ba._language import Lstr
@ -882,61 +900,61 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
t_offs = -350.0 t_offs = -350.0
height_offs = 100.0 height_offs = 100.0
tnode = ba.newnode('text', tnode = ba.newnode('text',
attrs={ attrs={
'text': tip_lstr, 'text': tip_lstr,
'scale': tip_scale, 'scale': tip_scale,
'maxwidth': 900, 'maxwidth': 900,
'position': (base_position[0] + t_offs, 'position': (base_position[0] + t_offs,
base_position[1] + height_offs), base_position[1] + height_offs),
'h_align': 'left', 'h_align': 'left',
'vr_depth': 300, 'vr_depth': 300,
'shadow': 1.0 if vrmode else 0.5, 'shadow': 1.0 if vrmode else 0.5,
'flatness': 1.0 if vrmode else 0.5, 'flatness': 1.0 if vrmode else 0.5,
'v_align': 'center', 'v_align': 'center',
'v_attach': 'bottom' 'v_attach': 'bottom'
}) })
t2pos = (base_position[0] + t_offs - (20 if icon is None else 82), t2pos = (base_position[0] + t_offs - (20 if icon is None else 82),
base_position[1] + 2 + height_offs) base_position[1] + 2 + height_offs)
t2node = ba.newnode('text', t2node = ba.newnode('text',
owner=tnode, owner=tnode,
attrs={ attrs={
'text': tip_title, 'text': tip_title,
'scale': tip_title_scale, 'scale': tip_title_scale,
'position': t2pos, 'position': t2pos,
'h_align': 'right', 'h_align': 'right',
'vr_depth': 300, 'vr_depth': 300,
'shadow': 1.0 if vrmode else 0.5, 'shadow': 1.0 if vrmode else 0.5,
'flatness': 1.0 if vrmode else 0.5, 'flatness': 1.0 if vrmode else 0.5,
'maxwidth': 140, 'maxwidth': 140,
'v_align': 'center', 'v_align': 'center',
'v_attach': 'bottom' 'v_attach': 'bottom'
}) })
if icon is not None: if icon is not None:
ipos = (base_position[0] + t_offs - 40, base_position[1] + 1 + height_offs) ipos = (base_position[0] + t_offs - 40, base_position[1] + 1 + height_offs)
img = ba.newnode('image', img = ba.newnode('image',
attrs={ attrs={
'texture': icon, 'texture': icon,
'position': ipos, 'position': ipos,
'scale': (50, 50), 'scale': (50, 50),
'opacity': 1.0, 'opacity': 1.0,
'vr_depth': 315, 'vr_depth': 315,
'color': (1, 1, 1), 'color': (1, 1, 1),
'absolute_scale': True, 'absolute_scale': True,
'attach': 'bottomCenter' 'attach': 'bottomCenter'
}) })
animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0})
ba.timer(5.0, img.delete) ba.timer(5.0, img.delete)
if sound is not None: if sound is not None:
ba.playsound(sound) ba.playsound(sound)
combine = ba.newnode('combine', combine = ba.newnode('combine',
owner=tnode, owner=tnode,
attrs={ attrs={
'input0': 1.0, 'input0': 1.0,
'input1': 0.8, 'input1': 0.8,
'input2': 1.0, 'input2': 1.0,
'size': 4 'size': 4
}) })
combine.connectattr('output', tnode, 'color') combine.connectattr('output', tnode, 'color')
combine.connectattr('output', t2node, 'color') combine.connectattr('output', t2node, 'color')
animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0})
@ -948,78 +966,79 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
# If the leaving player is marked, remove the mark # If the leaving player is marked, remove the mark
if player.state == PlayerState.MARKED: if player.state == PlayerState.MARKED:
self.remove_mark(player) self.remove_mark(player)
# If the leaving player is stunned, remove all stun timers # If the leaving player is stunned, remove all stun timers
elif player.state == PlayerState.STUNNED: elif player.state == PlayerState.STUNNED:
player.stunned_timer = None player.stunned_timer = None
player.stunned_update_timer = None player.stunned_update_timer = None
if len(self.get_marked_players()) == len(self.get_alive_players()): if len(self.get_marked_players()) == len(self.get_alive_players()):
for i in self.get_marked_players(): for i in self.get_marked_players():
self.remove_mark(i) self.remove_mark(i)
if len(self.get_marked_players()) == 0: if len(self.get_marked_players()) == 0:
self.marked_tick_timer = None self.marked_tick_timer = None
self.marked_players_died() self.marked_players_died()
player.set_state(PlayerState.ELIMINATED) player.set_state(PlayerState.ELIMINATED)
# This function is called every time a player spawns # This function is called every time a player spawns
def spawn_player(self, player: Player) -> ba.Actor: def spawn_player(self, player: Player) -> ba.Actor:
position = self.map.get_ffa_start_position(self.players) position = self.map.get_ffa_start_position(self.players)
position = (position[0], position = (position[0],
position[1] - 0.3, # Move the spawn a bit lower position[1] - 0.3, # Move the spawn a bit lower
position[2]) position[2])
name = player.getname() name = player.getname()
light_color = ba.normalized_color(player.color) light_color = ba.normalized_color(player.color)
display_color = ba.safecolor(player.color, target_intensity=0.75) display_color = ba.safecolor(player.color, target_intensity=0.75)
# Here we actually crate the player character # Here we actually crate the player character
spaz = PotatoPlayerSpaz(color=player.color, spaz = PotatoPlayerSpaz(color=player.color,
highlight=player.highlight, highlight=player.highlight,
character=player.character, character=player.character,
player=player) player=player)
spaz.node.invincible = False # Immediately turn off invincibility spaz.node.invincible = False # Immediately turn off invincibility
player.actor = spaz # Assign player character to the owner player.actor = spaz # Assign player character to the owner
spaz.node.name = name spaz.node.name = name
spaz.node.name_color = display_color spaz.node.name_color = display_color
spaz.connect_controls_to_player() spaz.connect_controls_to_player()
# Move to the stand position and add a flash of light # Move to the stand position and add a flash of light
spaz.handlemessage(ba.StandMessage(position, random.uniform(0, 360))) spaz.handlemessage(ba.StandMessage(position, random.uniform(0, 360)))
t = ba.time(ba.TimeType.BASE) t = ba.time(ba.TimeType.BASE)
ba.playsound(self._spawn_sound, 1.0, position=spaz.node.position) ba.playsound(self._spawn_sound, 1.0, position=spaz.node.position)
light = ba.newnode('light', attrs={'color': light_color}) light = ba.newnode('light', attrs={'color': light_color})
spaz.node.connectattr('position', light, 'position') spaz.node.connectattr('position', light, 'position')
ba.animate(light, 'intensity', {0: 0, ba.animate(light, 'intensity', {0: 0,
0.25: 1, 0.25: 1,
0.5: 0}) 0.5: 0})
ba.timer(0.5, light.delete) ba.timer(0.5, light.delete)
# Game reacts to various events # Game reacts to various events
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
# This is called if the player dies. # This is called if the player dies.
if isinstance(msg, ba.PlayerDiedMessage): if isinstance(msg, ba.PlayerDiedMessage):
super().handlemessage(msg) # super().handlemessage(msg)
player = msg.getplayer(Player) player = msg.getplayer(Player)
# If a player gets eliminated, don't respawn # If a player gets eliminated, don't respawn
if msg.how == 'marked_elimination': return if msg.how == 'marked_elimination':
return
self.spawn_player(player) # Spawn a new player character
self.spawn_player(player) # Spawn a new player character
# If a REGULAR player dies, they respawn STUNNED. # If a REGULAR player dies, they respawn STUNNED.
# If a STUNNED player dies, reapply all visual effects. # If a STUNNED player dies, reapply all visual effects.
if player.state in [PlayerState.REGULAR, PlayerState.STUNNED]: if player.state in [PlayerState.REGULAR, PlayerState.STUNNED]:
player.set_state(PlayerState.STUNNED) player.set_state(PlayerState.STUNNED)
# If a MARKED player falls off the map, apply the MARKED effects on the new spaz that respawns. # If a MARKED player falls off the map, apply the MARKED effects on the new spaz that respawns.
if player.state == PlayerState.MARKED: if player.state == PlayerState.MARKED:
self.mark(player) self.mark(player)
# This is called when we want to end the game and announce a victor # This is called when we want to end the game and announce a victor
def end_game(self) -> None: def end_game(self) -> None:
# Proceed only if the game hasn't ended yet. # Proceed only if the game hasn't ended yet.
@ -1035,4 +1054,4 @@ class HotPotato(ba.TeamGameActivity[Player, ba.Team]):
# Use each player's index in the array for our scoring # Use each player's index in the array for our scoring
# 0 is the first index, so we add 1 to the score. # 0 is the first index, so we add 1 to the score.
results.set_team_score(team, self.match_placement.index(team) + 1) results.set_team_score(team, self.match_placement.index(team) + 1)
self.end(results=results) # Standard game ending behavior self.end(results=results) # Standard game ending behavior

View file

@ -22,38 +22,41 @@ from bastd.actor.bomb import Bomb
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
# ba_meta export plugin # ba_meta export plugin
class BombRadiusVisualizer(ba.Plugin): class BombRadiusVisualizer(ba.Plugin):
# We use a decorator to add extra code to existing code, increasing mod compatibility. # 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. # Here I'm defining a new bomb init function that'll be replaced.
def new_bomb_init(func): def new_bomb_init(func):
# This function will return our wrapper function, which is going to take the original function's base arguments. # 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. # 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. # 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 # We're working kind of blindly here, so it's good to have the original function
# open in a second window for argument reference. # open in a second window for argument reference.
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
# Here's where we execute the original game's code, so it's not lost. # 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. # We want to add our code at the end of the existing code, so our code goes under that.
func(*args, **kwargs) 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. # 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. # 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. # We aren't defining the size, because we're gonna animate it shortly after.
args[0].radius_visualizer = ba.newnode('locator', args[0].radius_visualizer = ba.newnode('locator',
owner=args[0].node, # Remove itself when the bomb node dies. # Remove itself when the bomb node dies.
attrs={ owner=args[0].node,
'shape': 'circle', attrs={
'color': (1, 0, 0), 'shape': 'circle',
'opacity':0.05, 'color': (1, 0, 0),
'draw_beauty': False, 'opacity': 0.05,
'additive': False 'draw_beauty': False,
}) 'additive': False
})
# Let's connect our circle to the bomb. # Let's connect our circle to the bomb.
args[0].node.connectattr('position', args[0].radius_visualizer, 'position') 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. # 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. # 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, { ba.animate_array(args[0].radius_visualizer, 'size', 1, {
@ -61,20 +64,22 @@ class BombRadiusVisualizer(ba.Plugin):
0.2: [args[0].blast_radius * 2.2], 0.2: [args[0].blast_radius * 2.2],
0.25: [args[0].blast_radius * 2.0] 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. # 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', args[0].radius_visualizer_circle = ba.newnode('locator',
owner=args[0].node, # Remove itself when the bomb node dies. # Remove itself when the bomb node dies.
attrs={ owner=args[0].node,
'shape': 'circleOutline', attrs={
'size':[args[0].blast_radius * 2.0], # Here's that bomb's blast radius value again! 'shape': 'circleOutline',
'color': (1, 1, 0), # Here's that bomb's blast radius value again!
'draw_beauty': False, 'size': [args[0].blast_radius * 2.0],
'additive': True 'color': (1, 1, 0),
}) 'draw_beauty': False,
'additive': True
})
# Attach the circle to the bomb. # Attach the circle to the bomb.
args[0].node.connectattr('position', args[0].radius_visualizer_circle, 'position') 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. # Let's animate that circle too, but this time let's do the opacity.
ba.animate( ba.animate(
args[0].radius_visualizer_circle, 'opacity', { args[0].radius_visualizer_circle, 'opacity', {
@ -82,8 +87,7 @@ class BombRadiusVisualizer(ba.Plugin):
0.4: 0.1 0.4: 0.1
}) })
return wrapper return wrapper
# Finally we """travel through the game files""" to replace the function we want with our own version. # 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. # We transplant the old function's arguments into our version.
bastd.actor.bomb.Bomb.__init__ = new_bomb_init(bastd.actor.bomb.Bomb.__init__) bastd.actor.bomb.Bomb.__init__ = new_bomb_init(bastd.actor.bomb.Bomb.__init__)

View file

@ -24,107 +24,113 @@ if TYPE_CHECKING:
pass pass
# ba_meta export plugin # ba_meta export plugin
class Quickturn(ba.Plugin): class Quickturn(ba.Plugin):
class FootConnectMessage: class FootConnectMessage:
"""Spaz started touching the ground""" """Spaz started touching the ground"""
class FootDisconnectMessage: class FootDisconnectMessage:
"""Spaz stopped touching the ground""" """Spaz stopped touching the ground"""
def wavedash(self) -> None: def wavedash(self) -> None:
if not self.node: if not self.node:
return return
isMoving = abs(self.node.move_up_down) >= 0.5 or abs(self.node.move_left_right) >= 0.5 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: if self._dead or not self.grounded or not isMoving:
return return
if self.node.knockout > 0.0 or self.frozen or self.node.hold_node: if self.node.knockout > 0.0 or self.frozen or self.node.hold_node:
return return
t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
assert isinstance(t_ms, int) assert isinstance(t_ms, int)
if t_ms - self.last_wavedash_time_ms >= self._wavedash_cooldown: if t_ms - self.last_wavedash_time_ms >= self._wavedash_cooldown:
move = [self.node.move_left_right, -self.node.move_up_down] move = [self.node.move_left_right, -self.node.move_up_down]
vel = [self.node.velocity[0], self.node.velocity[2]] vel = [self.node.velocity[0], self.node.velocity[2]]
move_length = math.hypot(move[0], move[1]) move_length = math.hypot(move[0], move[1])
vel_length = math.hypot(vel[0], vel[1]) vel_length = math.hypot(vel[0], vel[1])
if vel_length < 0.6: return if vel_length < 0.6:
return
move_norm = [m/move_length for m in move] move_norm = [m/move_length for m in move]
vel_norm = [v/vel_length for v in vel] vel_norm = [v/vel_length for v in vel]
dot = sum(x*y for x,y in zip(move_norm,vel_norm)) 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) turn_power = min(round(math.acos(dot) / math.pi, 2)*1.3, 1)
# https://easings.net/#easeInOutQuart # https://easings.net/#easeInOutQuart
if turn_power < 0.55: if turn_power < 0.55:
turn_power = 8 * turn_power * turn_power * turn_power * turn_power turn_power = 8 * turn_power * turn_power * turn_power * turn_power
else: else:
turn_power = 0.55 - pow(-2 * turn_power + 2, 4) / 2 turn_power = 0.55 - pow(-2 * turn_power + 2, 4) / 2
if turn_power < 0.1: return 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) 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 self.last_wavedash_time_ms = t_ms
# FX # FX
ba.emitfx(position=self.node.position, ba.emitfx(position=self.node.position,
velocity=(vel[0]*0.5,-1,vel[1]*0.5), velocity=(vel[0]*0.5, -1, vel[1]*0.5),
chunk_type='spark', chunk_type='spark',
count=5, count=5,
scale=boost_power / 160 * turn_power, scale=boost_power / 160 * turn_power,
spread=0.25); spread=0.25)
# Boost itself # Boost itself
pos = self.node.position pos = self.node.position
for i in range(6): for i in range(6):
self.node.handlemessage('impulse',pos[0],0.2+pos[1]+i*0.1,pos[2], self.node.handlemessage('impulse', pos[0], 0.2+pos[1]+i*0.1, pos[2],
0,0,0, 0, 0, 0,
boost_power * turn_power, boost_power * turn_power,
boost_power * turn_power,0,0, boost_power * turn_power, 0, 0,
move[0],0,move[1]) move[0], 0, move[1])
def new_spaz_init(func): def new_spaz_init(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
func(*args, **kwargs) func(*args, **kwargs)
# args[0] = self # args[0] = self
args[0]._wavedash_cooldown = 170 args[0]._wavedash_cooldown = 170
args[0].last_wavedash_time_ms = -9999 args[0].last_wavedash_time_ms = -9999
args[0].grounded = 0 args[0].grounded = 0
return wrapper return wrapper
bastd.actor.spaz.Spaz.__init__ = new_spaz_init(bastd.actor.spaz.Spaz.__init__) bastd.actor.spaz.Spaz.__init__ = new_spaz_init(bastd.actor.spaz.Spaz.__init__)
def new_factory(func): def new_factory(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
func(*args, **kwargs) func(*args, **kwargs)
args[0].roller_material.add_actions( args[0].roller_material.add_actions(
conditions=('they_have_material', bastd.gameutils.SharedObjects.get().footing_material), conditions=('they_have_material',
actions=(('message', 'our_node', 'at_connect', Quickturn.FootConnectMessage), bastd.gameutils.SharedObjects.get().footing_material),
('message', 'our_node', 'at_disconnect', Quickturn.FootDisconnectMessage))) actions=(('message', 'our_node', 'at_connect', Quickturn.FootConnectMessage),
('message', 'our_node', 'at_disconnect', Quickturn.FootDisconnectMessage)))
return wrapper return wrapper
bastd.actor.spazfactory.SpazFactory.__init__ = new_factory(bastd.actor.spazfactory.SpazFactory.__init__) bastd.actor.spazfactory.SpazFactory.__init__ = new_factory(
bastd.actor.spazfactory.SpazFactory.__init__)
def new_handlemessage(func): def new_handlemessage(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if args[1] == Quickturn.FootConnectMessage: if args[1] == Quickturn.FootConnectMessage:
args[0].grounded += 1 args[0].grounded += 1
elif args[1] == Quickturn.FootDisconnectMessage: elif args[1] == Quickturn.FootDisconnectMessage:
if args[0].grounded > 0: args[0].grounded -= 1 if args[0].grounded > 0:
args[0].grounded -= 1
func(*args, **kwargs) func(*args, **kwargs)
return wrapper return wrapper
bastd.actor.spaz.Spaz.handlemessage = new_handlemessage(bastd.actor.spaz.Spaz.handlemessage) bastd.actor.spaz.Spaz.handlemessage = new_handlemessage(bastd.actor.spaz.Spaz.handlemessage)
def new_on_run(func): def new_on_run(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if args[0]._last_run_value < args[1] and args[1] > 0.8: if args[0]._last_run_value < args[1] and args[1] > 0.8:

View file

@ -28,6 +28,8 @@ if TYPE_CHECKING:
pass pass
# ba_meta export plugin # ba_meta export plugin
class RagdollBGone(ba.Plugin): class RagdollBGone(ba.Plugin):
# We use a decorator to add extra code to existing code, increasing mod compatibility. # We use a decorator to add extra code to existing code, increasing mod compatibility.
@ -37,23 +39,23 @@ class RagdollBGone(ba.Plugin):
# This function will return our wrapper function, which is going to take the original function's base arguments. # 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. # 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. # 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 # We're working kind of blindly here, so it's good to have the original function
# open in a second window for argument reference. # open in a second window for argument reference.
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if isinstance(args[1], ba.DieMessage): # Replace Spaz death behavior if isinstance(args[1], ba.DieMessage): # Replace Spaz death behavior
# Here we play the gamey death noise in Co-op. # Here we play the gamey death noise in Co-op.
if not args[1].immediate: if not args[1].immediate:
if args[0].play_big_death_sound and not args[0]._dead: if args[0].play_big_death_sound and not args[0]._dead:
ba.playsound(SpazFactory.get().single_player_death_sound) ba.playsound(SpazFactory.get().single_player_death_sound)
# If our Spaz dies by falling out of the map, we want to keep the ragdoll. # 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. # 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: 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. # 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! # We can change their values though!
# "hurt" property is basically the health bar above the player and the blinking when low on health. # "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. # 1.0 means empty health bar and the fastest blinking in the west.
args[0].node.hurt = 1.0 args[0].node.hurt = 1.0
# Make our Spaz close their eyes permanently and then make their body disintegrate. # 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. # Again, this behavior is built in. We can only trigger it by setting "dead" to True.
@ -78,29 +80,34 @@ class RagdollBGone(ba.Plugin):
args[0].node.position[2]) args[0].node.position[2])
# This function allows us to spawn particles like sparks and bomb shrapnel. # This function allows us to spawn particles like sparks and bomb shrapnel.
# We're gonna use sparks here. # We're gonna use sparks here.
ba.emitfx(position = pos, # Here we place our edited position. ba.emitfx(position=pos, # Here we place our edited position.
velocity=args[0].node.velocity, velocity=args[0].node.velocity,
count=random.randrange(2, 5), # Random amount of sparks between 2 and 5 # Random amount of sparks between 2 and 5
scale=3.0, count=random.randrange(2, 5),
spread=0.2, scale=3.0,
chunk_type='spark') spread=0.2,
chunk_type='spark')
# Make a Spaz death noise if we're not gibbed. # Make a Spaz death noise if we're not gibbed.
if not args[0].shattered: if not args[0].shattered:
death_sounds = args[0].node.death_sounds # Get our Spaz's death noises, these change depending on character skins # 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 death_sounds = args[0].node.death_sounds
ba.playsound(sound, position=args[0].node.position) # Play the sound where our Spaz is # Pick a random death noise
sound = death_sounds[random.randrange(len(death_sounds))]
# Play the sound where our Spaz is
ba.playsound(sound, position=args[0].node.position)
# Delete our Spaz node immediately. # Delete our Spaz node immediately.
# Removing stuff is weird and prone to errors, so we're gonna delay it. # Removing stuff is weird and prone to errors, so we're gonna delay it.
ba.timer(0.001, args[0].node.delete) ba.timer(0.001, args[0].node.delete)
# Let's mark our Spaz as dead, so he can't die again. # 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. # 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. # "self.node" is a visual representation of the character while "self" is his game logic.
args[0]._dead = True args[0]._dead = True
args[0].hitpoints = 0 # Set his health to zero. This value is independent from the health bar above his head. # Set his health to zero. This value is independent from the health bar above his head.
args[0].hitpoints = 0
return return
# Worry no longer! We're not gonna remove all the base game code! # Worry no longer! We're not gonna remove all the base game code!
# Here's where we bring it all back. # 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. # 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.