Added new files

This commit is contained in:
vortex 2024-02-20 23:04:51 +05:30
parent 867634cc5c
commit 3a407868d4
1775 changed files with 550222 additions and 0 deletions

View file

@ -0,0 +1 @@
# Released under the MIT License. See LICENSE for details.

View file

@ -0,0 +1,145 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
import random
import weakref
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any
class Background(ba.Actor):
"""Simple Fading Background Actor."""
def __init__(
self,
fade_time: float = 0.5,
start_faded: bool = False,
show_logo: bool = False,
):
super().__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 = ba.getsession()
self._session = weakref.ref(session)
with ba.Context(session):
self.node = ba.newnode(
'image',
delegate=self,
attrs={
'fill_screen': True,
'texture': ba.gettexture('bg'),
'tilt_translate': -0.3,
'has_alpha_channel': False,
'color': (1, 1, 1),
},
)
if not start_faded:
ba.animate(
self.node,
'opacity',
{0.0: 0.0, self.fade_time: 1.0},
loop=False,
)
if show_logo:
logo_texture = ba.gettexture('logo')
logo_model = ba.getmodel('logo')
logo_model_transparent = ba.getmodel('logoTransparent')
self.logo = ba.newnode(
'image',
owner=self.node,
attrs={
'texture': logo_texture,
'model_opaque': logo_model,
'model_transparent': logo_model_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 ba.app.vr_mode:
self.cmb = ba.newnode(
'combine', owner=self.node, attrs={'size': 2}
)
for attr in ['input0', 'input1']:
ba.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 = ba.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
ba.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
ba.animate(cmb, 'input1', keys, loop=True)
def __del__(self) -> None:
# Normal actors don't get sent DieMessages when their
# activity is shutting down, but we still need to do so
# since our node lives in the session and it wouldn't die
# otherwise.
self._die()
super().__del__()
def _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)
ba.print_error(
'got None session on Background _die'
' (and node still exists!)'
)
elif session is not None:
with ba.Context(session):
if not self._dying and self.node:
self._dying = True
if immediate:
self.node.delete()
else:
ba.animate(
self.node,
'opacity',
{0.0: 1.0, self.fade_time: 0.0},
loop=False,
)
ba.timer(self.fade_time + 0.1, self.node.delete)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
self._die(msg.immediate)
else:
super().handlemessage(msg)

1212
dist/ba_data/python/bastd/actor/bomb.py vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,639 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actors related to controls guides."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any, Sequence
class ControlsGuide(ba.Actor):
"""A screen overlay of game controls.
category: Gameplay Classes
Shows button mappings based on what controllers are connected.
Handy to show at the start of a series or whenever there might
be newbies watching.
"""
def __init__(
self,
position: tuple[float, float] = (390.0, 120.0),
scale: float = 1.0,
delay: float = 0.0,
lifespan: float | None = None,
bright: bool = False,
):
"""Instantiate an overlay.
delay: is the time in seconds before the overlay fades in.
lifespan: if not None, the overlay will fade back out and die after
that long (in milliseconds).
bright: if True, brighter colors will be used; handy when showing
over gameplay but may be too bright for join-screens, etc.
"""
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
super().__init__()
show_title = True
scale *= 0.75
image_size = 90.0 * scale
offs = 74.0 * scale
offs5 = 43.0 * scale
ouya = False
maxw = 50
self._lifespan = lifespan
self._dead = False
self._bright = bright
self._cancel_timer: ba.Timer | None = None
self._fade_in_timer: ba.Timer | None = None
self._update_timer: ba.Timer | None = None
self._title_text: ba.Node | None
clr: Sequence[float]
extra_pos_1: tuple[float, float] | None
extra_pos_2: tuple[float, float] | None
if ba.app.iircade_mode:
xtweak = 0.2
ytweak = 0.2
jump_pos = (
position[0] + offs * (-1.2 + xtweak),
position[1] + offs * (0.1 + ytweak),
)
bomb_pos = (
position[0] + offs * (0.0 + xtweak),
position[1] + offs * (0.5 + ytweak),
)
punch_pos = (
position[0] + offs * (1.2 + xtweak),
position[1] + offs * (0.5 + ytweak),
)
pickup_pos = (
position[0] + offs * (-1.4 + xtweak),
position[1] + offs * (-1.2 + ytweak),
)
extra_pos_1 = (
position[0] + offs * (-0.2 + xtweak),
position[1] + offs * (-0.8 + ytweak),
)
extra_pos_2 = (
position[0] + offs * (1.0 + xtweak),
position[1] + offs * (-0.8 + ytweak),
)
self._force_hide_button_names = True
else:
punch_pos = (position[0] - offs * 1.1, position[1])
jump_pos = (position[0], position[1] - offs)
bomb_pos = (position[0] + offs * 1.1, position[1])
pickup_pos = (position[0], position[1] + offs)
extra_pos_1 = None
extra_pos_2 = None
self._force_hide_button_names = False
if show_title:
self._title_text_pos_top = (
position[0],
position[1] + 139.0 * scale,
)
self._title_text_pos_bottom = (
position[0],
position[1] + 139.0 * scale,
)
clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
tval = ba.Lstr(
value='${A}:', subs=[('${A}', ba.Lstr(resource='controlsText'))]
)
self._title_text = ba.newnode(
'text',
attrs={
'text': tval,
'host_only': True,
'scale': 1.1 * scale,
'shadow': 0.5,
'flatness': 1.0,
'maxwidth': 480,
'v_align': 'center',
'h_align': 'center',
'color': clr,
},
)
else:
self._title_text = None
pos = jump_pos
clr = (0.4, 1, 0.4)
self._jump_image = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('buttonJump'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr,
},
)
self._jump_text = ba.newnode(
'text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3)
pos = punch_pos
self._punch_image = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('buttonPunch'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr,
},
)
self._punch_text = ba.newnode(
'text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
pos = bomb_pos
clr = (1, 0.3, 0.3)
self._bomb_image = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('buttonBomb'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr,
},
)
self._bomb_text = ba.newnode(
'text',
attrs={
'h_align': 'center',
'v_align': 'top',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
pos = pickup_pos
clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1)
self._pickup_image = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('buttonPickUp'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr,
},
)
self._pick_up_text = ba.newnode(
'text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0)
self._run_text_pos_top = (position[0], position[1] - 135.0 * scale)
self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale)
sval = 1.0 * scale if ba.app.vr_mode else 0.8 * scale
self._run_text = ba.newnode(
'text',
attrs={
'scale': sval,
'host_only': True,
'shadow': 1.0 if ba.app.vr_mode else 0.5,
'flatness': 1.0,
'maxwidth': 380,
'v_align': 'top',
'h_align': 'center',
'color': clr,
},
)
clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
self._extra_text = ba.newnode(
'text',
attrs={
'scale': 0.8 * scale,
'host_only': True,
'shadow': 0.5,
'flatness': 1.0,
'maxwidth': 380,
'v_align': 'top',
'h_align': 'center',
'color': clr,
},
)
if extra_pos_1 is not None:
self._extra_image_1: ba.Node | None = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('nub'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': extra_pos_1,
'scale': (image_size, image_size),
'color': (0.5, 0.5, 0.5),
},
)
else:
self._extra_image_1 = None
if extra_pos_2 is not None:
self._extra_image_2: ba.Node | None = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('nub'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': extra_pos_2,
'scale': (image_size, image_size),
'color': (0.5, 0.5, 0.5),
},
)
else:
self._extra_image_2 = None
self._nodes = [
self._bomb_image,
self._bomb_text,
self._punch_image,
self._punch_text,
self._jump_image,
self._jump_text,
self._pickup_image,
self._pick_up_text,
self._run_text,
self._extra_text,
]
if show_title:
assert self._title_text
self._nodes.append(self._title_text)
if self._extra_image_1 is not None:
self._nodes.append(self._extra_image_1)
if self._extra_image_2 is not None:
self._nodes.append(self._extra_image_2)
# Start everything invisible.
for node in self._nodes:
node.opacity = 0.0
# Don't do anything until our delay has passed.
ba.timer(delay, ba.WeakCall(self._start_updating))
@staticmethod
def _meaningful_button_name(device: ba.InputDevice, button: int) -> str:
"""Return a flattened string button name; empty for non-meaningful."""
if not device.has_meaningful_button_names:
return ''
return device.get_button_name(button).evaluate()
def _start_updating(self) -> None:
# Ok, our delay has passed. Now lets periodically see if we can fade
# in (if a touch-screen is present we only want to show up if gamepads
# are connected, etc).
# Also set up a timer so if we haven't faded in by the end of our
# duration, abort.
if self._lifespan is not None:
self._cancel_timer = ba.Timer(
self._lifespan,
ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True)),
)
self._fade_in_timer = ba.Timer(
1.0, ba.WeakCall(self._check_fade_in), repeat=True
)
self._check_fade_in() # Do one check immediately.
def _check_fade_in(self) -> None:
from ba.internal import get_device_value
# If we have a touchscreen, we only fade in if we have a player with
# an input device that is *not* the touchscreen.
# (otherwise it is confusing to see the touchscreen buttons right
# next to our display buttons)
touchscreen: ba.InputDevice | None = ba.internal.getinputdevice(
'TouchScreen', '#1', doraise=False
)
if touchscreen is not None:
# We look at the session's players; not the activity's.
# We want to get ones who are still in the process of
# selecting a character, etc.
input_devices = [
p.inputdevice for p in ba.getsession().sessionplayers
]
input_devices = [
i for i in input_devices if i and i is not touchscreen
]
fade_in = False
if input_devices:
# Only count this one if it has non-empty button names
# (filters out wiimotes, the remote-app, etc).
for device in input_devices:
for name in (
'buttonPunch',
'buttonJump',
'buttonBomb',
'buttonPickUp',
):
if (
self._meaningful_button_name(
device, get_device_value(device, name)
)
!= ''
):
fade_in = True
break
if fade_in:
break # No need to keep looking.
else:
# No touch-screen; fade in immediately.
fade_in = True
if fade_in:
self._cancel_timer = None # Didn't need this.
self._fade_in_timer = None # Done with this.
self._fade_in()
def _fade_in(self) -> None:
for node in self._nodes:
ba.animate(node, 'opacity', {0: 0.0, 2.0: 1.0})
# If we were given a lifespan, transition out after it.
if self._lifespan is not None:
ba.timer(
self._lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())
)
self._update()
self._update_timer = ba.Timer(
1.0, ba.WeakCall(self._update), repeat=True
)
def _update(self) -> None:
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
from ba.internal import get_device_value, get_remote_app_name
if self._dead:
return
punch_button_names = set()
jump_button_names = set()
pickup_button_names = set()
bomb_button_names = set()
# We look at the session's players; not the activity's - we want to
# get ones who are still in the process of selecting a character, etc.
input_devices = [p.inputdevice for p in ba.getsession().sessionplayers]
input_devices = [i for i in input_devices if i]
# If there's no players with input devices yet, try to default to
# showing keyboard controls.
if not input_devices:
kbd = ba.internal.getinputdevice('Keyboard', '#1', doraise=False)
if kbd is not None:
input_devices.append(kbd)
# We word things specially if we have nothing but keyboards.
all_keyboards = input_devices and all(
i.name == 'Keyboard' for i in input_devices
)
only_remote = len(input_devices) == 1 and all(
i.name == 'Amazon Fire TV Remote' for i in input_devices
)
right_button_names = set()
left_button_names = set()
up_button_names = set()
down_button_names = set()
# For each player in the game with an input device,
# get the name of the button for each of these 4 actions.
# If any of them are uniform across all devices, display the name.
for device in input_devices:
# We only care about movement buttons in the case of keyboards.
if all_keyboards:
right_button_names.add(
device.get_button_name(
get_device_value(device, 'buttonRight')
)
)
left_button_names.add(
device.get_button_name(
get_device_value(device, 'buttonLeft')
)
)
down_button_names.add(
device.get_button_name(
get_device_value(device, 'buttonDown')
)
)
up_button_names.add(
device.get_button_name(get_device_value(device, 'buttonUp'))
)
# Ignore empty values; things like the remote app or
# wiimotes can return these.
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonPunch')
)
if bname != '':
punch_button_names.add(bname)
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonJump')
)
if bname != '':
jump_button_names.add(bname)
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonBomb')
)
if bname != '':
bomb_button_names.add(bname)
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonPickUp')
)
if bname != '':
pickup_button_names.add(bname)
# If we have no values yet, we may want to throw out some sane
# defaults.
if all(
not lst
for lst in (
punch_button_names,
jump_button_names,
bomb_button_names,
pickup_button_names,
)
):
# Otherwise on android show standard buttons.
if ba.app.platform == 'android':
punch_button_names.add('X')
jump_button_names.add('A')
bomb_button_names.add('B')
pickup_button_names.add('Y')
run_text = ba.Lstr(
value='${R}: ${B}',
subs=[
('${R}', ba.Lstr(resource='runText')),
(
'${B}',
ba.Lstr(
resource='holdAnyKeyText'
if all_keyboards
else 'holdAnyButtonText'
),
),
],
)
# If we're all keyboards, lets show move keys too.
if (
all_keyboards
and len(up_button_names) == 1
and len(down_button_names) == 1
and len(left_button_names) == 1
and len(right_button_names) == 1
):
up_text = list(up_button_names)[0]
down_text = list(down_button_names)[0]
left_text = list(left_button_names)[0]
right_text = list(right_button_names)[0]
run_text = ba.Lstr(
value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}',
subs=[
('${M}', ba.Lstr(resource='moveText')),
('${U}', up_text),
('${L}', left_text),
('${D}', down_text),
('${R}', right_text),
('${RUN}', run_text),
],
)
if self._force_hide_button_names:
jump_button_names.clear()
punch_button_names.clear()
bomb_button_names.clear()
pickup_button_names.clear()
self._run_text.text = run_text
w_text: ba.Lstr | str
if only_remote and self._lifespan is None:
w_text = ba.Lstr(
resource='fireTVRemoteWarningText',
subs=[('${REMOTE_APP_NAME}', get_remote_app_name())],
)
else:
w_text = ''
self._extra_text.text = w_text
if len(punch_button_names) == 1:
self._punch_text.text = list(punch_button_names)[0]
else:
self._punch_text.text = ''
if len(jump_button_names) == 1:
tval = list(jump_button_names)[0]
else:
tval = ''
self._jump_text.text = tval
if tval == '':
self._run_text.position = self._run_text_pos_top
self._extra_text.position = (
self._run_text_pos_top[0],
self._run_text_pos_top[1] - 50,
)
else:
self._run_text.position = self._run_text_pos_bottom
self._extra_text.position = (
self._run_text_pos_bottom[0],
self._run_text_pos_bottom[1] - 50,
)
if len(bomb_button_names) == 1:
self._bomb_text.text = list(bomb_button_names)[0]
else:
self._bomb_text.text = ''
# Also move our title up/down depending on if this is shown.
if len(pickup_button_names) == 1:
self._pick_up_text.text = list(pickup_button_names)[0]
if self._title_text is not None:
self._title_text.position = self._title_text_pos_top
else:
self._pick_up_text.text = ''
if self._title_text is not None:
self._title_text.position = self._title_text_pos_bottom
def _die(self) -> None:
for node in self._nodes:
node.delete()
self._nodes = []
self._update_timer = None
self._dead = True
def exists(self) -> bool:
return not self._dead
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if msg.immediate:
self._die()
else:
# If they don't need immediate,
# fade out our nodes and die later.
for node in self._nodes:
ba.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0})
ba.timer(3.1, ba.WeakCall(self._die))
return None
return super().handlemessage(msg)

380
dist/ba_data/python/bastd/actor/flag.py vendored Normal file
View file

@ -0,0 +1,380 @@
# Released under the MIT License. See LICENSE for details.
#
"""Implements a flag used for marking bases, capture-the-flag games, etc."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
import ba
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
class FlagFactory:
"""Wraps up media and other resources used by `Flag`s.
Category: **Gameplay Classes**
A single instance of this is shared between all flags
and can be retrieved via FlagFactory.get().
"""
flagmaterial: ba.Material
"""The ba.Material applied to all `Flag`s."""
impact_sound: ba.Sound
"""The ba.Sound used when a `Flag` hits the ground."""
skid_sound: ba.Sound
"""The ba.Sound used when a `Flag` skids along the ground."""
no_hit_material: ba.Material
"""A ba.Material that prevents contact with most objects;
applied to 'non-touchable' flags."""
flag_texture: ba.Texture
"""The ba.Texture for flags."""
_STORENAME = ba.storagename()
def __init__(self) -> None:
"""Instantiate a `FlagFactory`.
You shouldn't need to do this; call FlagFactory.get() to
get a shared instance.
"""
shared = SharedObjects.get()
self.flagmaterial = ba.Material()
self.flagmaterial.add_actions(
conditions=(
('we_are_younger_than', 100),
'and',
('they_have_material', shared.object_material),
),
actions=('modify_node_collision', 'collide', False),
)
self.flagmaterial.add_actions(
conditions=(
'they_have_material',
shared.footing_material,
),
actions=(
('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1),
),
)
self.impact_sound = ba.getsound('metalHit')
self.skid_sound = ba.getsound('metalSkid')
self.flagmaterial.add_actions(
conditions=(
'they_have_material',
shared.footing_material,
),
actions=(
('impact_sound', self.impact_sound, 2, 5),
('skid_sound', self.skid_sound, 2, 5),
),
)
self.no_hit_material = ba.Material()
self.no_hit_material.add_actions(
conditions=(
('they_have_material', shared.pickup_material),
'or',
('they_have_material', shared.attack_material),
),
actions=('modify_part_collision', 'collide', False),
)
# We also don't want anything moving it.
self.no_hit_material.add_actions(
conditions=(
('they_have_material', shared.object_material),
'or',
('they_dont_have_material', shared.footing_material),
),
actions=(
('modify_part_collision', 'collide', False),
('modify_part_collision', 'physical', False),
),
)
self.flag_texture = ba.gettexture("pixieIcon")
@classmethod
def get(cls) -> FlagFactory:
"""Get/create a shared `FlagFactory` instance."""
activity = ba.getactivity()
factory = activity.customdata.get(cls._STORENAME)
if factory is None:
factory = FlagFactory()
activity.customdata[cls._STORENAME] = factory
assert isinstance(factory, FlagFactory)
return factory
@dataclass
class FlagPickedUpMessage:
"""A message saying a `Flag` has been picked up.
Category: **Message Classes**
"""
flag: Flag
"""The `Flag` that has been picked up."""
node: ba.Node
"""The ba.Node doing the picking up."""
@dataclass
class FlagDiedMessage:
"""A message saying a `Flag` has died.
Category: **Message Classes**
"""
flag: Flag
"""The `Flag` that died."""
@dataclass
class FlagDroppedMessage:
"""A message saying a `Flag` has been dropped.
Category: **Message Classes**
"""
flag: Flag
"""The `Flag` that was dropped."""
node: ba.Node
"""The ba.Node that was holding it."""
class Flag(ba.Actor):
"""A flag; used in games such as capture-the-flag or king-of-the-hill.
Category: **Gameplay Classes**
Can be stationary or carry-able by players.
"""
def __init__(
self,
position: Sequence[float] = (0.0, 1.0, 0.0),
color: Sequence[float] = (1.0, 1.0, 1.0),
materials: Sequence[ba.Material] | None = None,
touchable: bool = True,
dropped_timeout: int | None = None,
):
"""Instantiate a flag.
If 'touchable' is False, the flag will only touch terrain;
useful for things like king-of-the-hill where players should
not be moving the flag around.
'materials can be a list of extra `ba.Material`s to apply to the flag.
If 'dropped_timeout' is provided (in seconds), the flag will die
after remaining untouched for that long once it has been moved
from its initial position.
"""
super().__init__()
self._initial_position: Sequence[float] | None = None
self._has_moved = False
shared = SharedObjects.get()
factory = FlagFactory.get()
if materials is None:
materials = []
elif not isinstance(materials, list):
# In case they passed a tuple or whatnot.
materials = list(materials)
if not touchable:
materials = [factory.no_hit_material] + materials
finalmaterials = [
shared.object_material,
factory.flagmaterial,
] + materials
self.node = ba.newnode(
'flag',
attrs={
'position': (position[0], position[1] + 0.75, position[2]),
'color_texture': factory.flag_texture,
'color': color,
'materials': finalmaterials,
},
delegate=self,
)
if dropped_timeout is not None:
dropped_timeout = int(dropped_timeout)
self._dropped_timeout = dropped_timeout
self._counter: ba.Node | None
if self._dropped_timeout is not None:
self._count = self._dropped_timeout
self._tick_timer = ba.Timer(
1.0, call=ba.WeakCall(self._tick), repeat=True
)
self._counter = ba.newnode(
'text',
owner=self.node,
attrs={
'in_world': True,
'color': (1, 1, 1, 0.7),
'scale': 0.015,
'shadow': 0.5,
'flatness': 1.0,
'h_align': 'center',
},
)
else:
self._counter = None
self._held_count = 0
self._score_text: ba.Node | None = None
self._score_text_hide_timer: ba.Timer | None = None
def _tick(self) -> None:
if self.node:
# Grab our initial position after one tick (in case we fall).
if self._initial_position is None:
self._initial_position = self.node.position
# Keep track of when we first move; we don't count down
# until then.
if not self._has_moved:
nodepos = self.node.position
if (
max(
abs(nodepos[i] - self._initial_position[i])
for i in list(range(3))
)
> 1.0
):
self._has_moved = True
if self._held_count > 0 or not self._has_moved:
assert self._dropped_timeout is not None
assert self._counter
self._count = self._dropped_timeout
self._counter.text = ''
else:
self._count -= 1
if self._count <= 10:
nodepos = self.node.position
assert self._counter
self._counter.position = (
nodepos[0],
nodepos[1] + 1.3,
nodepos[2],
)
self._counter.text = str(self._count)
if self._count < 1:
self.handlemessage(ba.DieMessage())
else:
assert self._counter
self._counter.text = ''
def _hide_score_text(self) -> None:
assert self._score_text is not None
assert isinstance(self._score_text.scale, float)
ba.animate(
self._score_text, 'scale', {0: self._score_text.scale, 0.2: 0}
)
def set_score_text(self, text: str) -> None:
"""Show a message over the flag; handy for scores."""
if not self.node:
return
if not self._score_text:
start_scale = 0.0
math = ba.newnode(
'math',
owner=self.node,
attrs={'input1': (0, 1.4, 0), 'operation': 'add'},
)
self.node.connectattr('position', math, 'input2')
self._score_text = ba.newnode(
'text',
owner=self.node,
attrs={
'text': text,
'in_world': True,
'scale': 0.02,
'shadow': 0.5,
'flatness': 1.0,
'h_align': 'center',
},
)
math.connectattr('output', self._score_text, 'position')
else:
assert isinstance(self._score_text.scale, float)
start_scale = self._score_text.scale
self._score_text.text = text
self._score_text.color = ba.safecolor(self.node.color)
ba.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02})
self._score_text_hide_timer = ba.Timer(
1.0, ba.WeakCall(self._hide_score_text)
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
if not msg.immediate:
self.activity.handlemessage(FlagDiedMessage(self))
elif isinstance(msg, ba.HitMessage):
assert self.node
assert msg.force_direction is not None
self.node.handlemessage(
'impulse',
msg.pos[0],
msg.pos[1],
msg.pos[2],
msg.velocity[0],
msg.velocity[1],
msg.velocity[2],
msg.magnitude,
msg.velocity_magnitude,
msg.radius,
0,
msg.force_direction[0],
msg.force_direction[1],
msg.force_direction[2],
)
elif isinstance(msg, ba.PickedUpMessage):
self._held_count += 1
if self._held_count == 1 and self._counter is not None:
self._counter.text = ''
self.activity.handlemessage(FlagPickedUpMessage(self, msg.node))
elif isinstance(msg, ba.DroppedMessage):
self._held_count -= 1
if self._held_count < 0:
print('Flag held count < 0.')
self._held_count = 0
self.activity.handlemessage(FlagDroppedMessage(self, msg.node))
else:
super().handlemessage(msg)
@staticmethod
def project_stand(pos: Sequence[float]) -> None:
"""Project a flag-stand onto the ground at the given position.
Useful for games such as capture-the-flag to show where a
movable flag originated from.
"""
assert len(pos) == 3
ba.emitfx(position=pos, emit_type='flag_stand')

174
dist/ba_data/python/bastd/actor/image.py vendored Normal file
View file

@ -0,0 +1,174 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence
class Image(ba.Actor):
"""Just a wrapped up image node with a few tricks up its sleeve."""
class Transition(Enum):
"""Transition types we support."""
FADE_IN = 'fade_in'
IN_RIGHT = 'in_right'
IN_LEFT = 'in_left'
IN_BOTTOM = 'in_bottom'
IN_BOTTOM_SLOW = 'in_bottom_slow'
IN_TOP_SLOW = 'in_top_slow'
class Attach(Enum):
"""Attach types we support."""
CENTER = 'center'
TOP_CENTER = 'topCenter'
TOP_LEFT = 'topLeft'
BOTTOM_CENTER = 'bottomCenter'
def __init__(
self,
texture: ba.Texture | dict[str, Any],
position: tuple[float, float] = (0, 0),
transition: Transition | None = None,
transition_delay: float = 0.0,
attach: Attach = Attach.CENTER,
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
scale: tuple[float, float] = (100.0, 100.0),
transition_out_delay: float | None = None,
model_opaque: ba.Model | None = None,
model_transparent: ba.Model | None = None,
vr_depth: float = 0.0,
host_only: bool = False,
front: bool = False,
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
super().__init__()
# If they provided a dict as texture, assume its an icon.
# otherwise its just a texture value itself.
mask_texture: ba.Texture | None
if isinstance(texture, dict):
tint_color = texture['tint_color']
tint2_color = texture['tint2_color']
tint_texture = texture['tint_texture']
texture = texture['texture']
mask_texture = ba.gettexture('characterIconMask')
else:
tint_color = (1, 1, 1)
tint2_color = None
tint_texture = None
mask_texture = None
self.node = ba.newnode(
'image',
attrs={
'texture': texture,
'tint_color': tint_color,
'tint_texture': tint_texture,
'position': position,
'vr_depth': vr_depth,
'scale': scale,
'mask_texture': mask_texture,
'color': color,
'absolute_scale': True,
'host_only': host_only,
'front': front,
'attach': attach.value,
},
delegate=self,
)
if model_opaque is not None:
self.node.model_opaque = model_opaque
if model_transparent is not None:
self.node.model_transparent = model_transparent
if tint2_color is not None:
self.node.tint2_color = tint2_color
if transition is self.Transition.FADE_IN:
keys = {transition_delay: 0, transition_delay + 0.5: color[3]}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = color[3]
keys[transition_delay + transition_out_delay + 0.5] = 0
ba.animate(self.node, 'opacity', keys)
cmb = self.position_combine = ba.newnode(
'combine', owner=self.node, attrs={'size': 2}
)
if transition is self.Transition.IN_RIGHT:
keys = {
transition_delay: position[0] + 1200,
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
ba.animate(cmb, 'input0', keys)
cmb.input1 = position[1]
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_LEFT:
keys = {
transition_delay: position[0] - 1200,
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = position[0]
keys[transition_delay + transition_out_delay + 200] = (
-position[0] - 1200
)
o_keys[transition_delay + transition_out_delay + 0.15] = 1.0
o_keys[transition_delay + transition_out_delay + 0.2] = 0.0
ba.animate(cmb, 'input0', keys)
cmb.input1 = position[1]
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_BOTTOM_SLOW:
keys = {transition_delay: -400, transition_delay + 3.5: position[1]}
o_keys = {transition_delay: 0.0, transition_delay + 2.0: 1.0}
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_BOTTOM:
keys = {transition_delay: -400, transition_delay + 0.2: position[1]}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = position[1]
keys[transition_delay + transition_out_delay + 0.2] = -400
o_keys[transition_delay + transition_out_delay + 0.15] = 1.0
o_keys[transition_delay + transition_out_delay + 0.2] = 0.0
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_TOP_SLOW:
keys = {transition_delay: 400, transition_delay + 3.5: position[1]}
o_keys = {transition_delay: 0.0, transition_delay + 1.0: 1.0}
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
else:
assert transition is self.Transition.FADE_IN or transition is None
cmb.input0 = position[0]
cmb.input1 = position[1]
cmb.connectattr('output', self.node, 'position')
# If we're transitioning out, die at the end of it.
if transition_out_delay is not None:
ba.timer(
transition_delay + transition_out_delay + 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()),
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
return None
return super().handlemessage(msg)

View file

@ -0,0 +1,107 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor Type(s)."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Callable
class OnScreenCountdown(ba.Actor):
"""A Handy On-Screen Timer.
category: Gameplay Classes
Useful for time-based games that count down to zero.
"""
def __init__(self, duration: int, endcall: Callable[[], Any] | None = None):
"""Duration is provided in seconds."""
super().__init__()
self._timeremaining = duration
self._ended = False
self._endcall = endcall
self.node = ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': 'center',
'h_align': 'center',
'color': (1, 1, 0.5, 1),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, -70),
'scale': 1.4,
'text': '',
},
)
self.inputnode = ba.newnode(
'timedisplay',
attrs={
'time2': duration * 1000,
'timemax': duration * 1000,
'timemin': 0,
},
)
self.inputnode.connectattr('output', self.node, 'text')
self._countdownsounds = {
10: ba.getsound('announceTen'),
9: ba.getsound('announceNine'),
8: ba.getsound('announceEight'),
7: ba.getsound('announceSeven'),
6: ba.getsound('announceSix'),
5: ba.getsound('announceFive'),
4: ba.getsound('announceFour'),
3: ba.getsound('announceThree'),
2: ba.getsound('announceTwo'),
1: ba.getsound('announceOne'),
}
self._timer: ba.Timer | None = None
def start(self) -> None:
"""Start the timer."""
globalsnode = ba.getactivity().globalsnode
globalsnode.connectattr('time', self.inputnode, 'time1')
self.inputnode.time2 = (
globalsnode.time + (self._timeremaining + 1) * 1000
)
self._timer = ba.Timer(1.0, self._update, repeat=True)
def on_expire(self) -> None:
super().on_expire()
# Release callbacks/refs.
self._endcall = None
def _update(self, forcevalue: int | None = None) -> None:
if forcevalue is not None:
tval = forcevalue
else:
self._timeremaining = max(0, self._timeremaining - 1)
tval = self._timeremaining
# if there's a countdown sound for this time that we
# haven't played yet, play it
if tval == 10:
assert self.node
assert isinstance(self.node.scale, float)
self.node.scale *= 1.2
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 4})
cmb.connectattr('output', self.node, 'color')
ba.animate(cmb, 'input0', {0: 1.0, 0.15: 1.0}, loop=True)
ba.animate(cmb, 'input1', {0: 1.0, 0.15: 0.5}, loop=True)
ba.animate(cmb, 'input2', {0: 0.1, 0.15: 0.0}, loop=True)
cmb.input3 = 1.0
if tval <= 10 and not self._ended:
ba.playsound(ba.getsound('tick'))
if tval in self._countdownsounds:
ba.playsound(self._countdownsounds[tval])
if tval <= 0 and not self._ended:
self._ended = True
if self._endcall is not None:
self._endcall()

View file

@ -0,0 +1,131 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
from typing import TYPE_CHECKING, overload
import ba
if TYPE_CHECKING:
from typing import Any, Literal
class OnScreenTimer(ba.Actor):
"""A handy on-screen timer.
category: Gameplay Classes
Useful for time-based games where time increases.
"""
def __init__(self) -> None:
super().__init__()
self._starttime_ms: int | None = None
self.node = ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': 'center',
'h_align': 'center',
'color': (1, 1, 0.5, 1),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, -70),
'scale': 1.4,
'text': '',
},
)
self.inputnode = ba.newnode(
'timedisplay', attrs={'timemin': 0, 'showsubseconds': True}
)
self.inputnode.connectattr('output', self.node, 'text')
def start(self) -> None:
"""Start the timer."""
tval = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
assert isinstance(tval, int)
self._starttime_ms = tval
self.inputnode.time1 = self._starttime_ms
ba.getactivity().globalsnode.connectattr(
'time', self.inputnode, 'time2'
)
def has_started(self) -> bool:
"""Return whether this timer has started yet."""
return self._starttime_ms is not None
def stop(
self,
endtime: int | float | None = None,
timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS,
) -> None:
"""End the timer.
If 'endtime' is not None, it is used when calculating
the final display time; otherwise the current time is used.
'timeformat' applies to endtime and can be SECONDS or MILLISECONDS
"""
if endtime is None:
endtime = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
timeformat = ba.TimeFormat.MILLISECONDS
if self._starttime_ms is None:
print('Warning: OnScreenTimer.stop() called without start() first')
else:
endtime_ms: int
if timeformat is ba.TimeFormat.SECONDS:
endtime_ms = int(endtime * 1000)
elif timeformat is ba.TimeFormat.MILLISECONDS:
assert isinstance(endtime, int)
endtime_ms = endtime
else:
raise ValueError(f'invalid timeformat: {timeformat}')
self.inputnode.timemax = endtime_ms - self._starttime_ms
# Overloads so type checker knows our exact return type based in args.
@overload
def getstarttime(
self, timeformat: Literal[ba.TimeFormat.SECONDS] = ba.TimeFormat.SECONDS
) -> float:
...
@overload
def getstarttime(
self, timeformat: Literal[ba.TimeFormat.MILLISECONDS]
) -> int:
...
def getstarttime(
self, timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS
) -> int | float:
"""Return the sim-time when start() was called.
Time will be returned in seconds if timeformat is SECONDS or
milliseconds if it is MILLISECONDS.
"""
val_ms: Any
if self._starttime_ms is None:
print('WARNING: getstarttime() called on un-started timer')
val_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
else:
val_ms = self._starttime_ms
assert isinstance(val_ms, int)
if timeformat is ba.TimeFormat.SECONDS:
return 0.001 * val_ms
if timeformat is ba.TimeFormat.MILLISECONDS:
return val_ms
raise ValueError(f'invalid timeformat: {timeformat}')
@property
def starttime(self) -> float:
"""Shortcut for start time in seconds."""
return self.getstarttime()
def handlemessage(self, msg: Any) -> Any:
# if we're asked to die, just kill our node/timer
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()

View file

@ -0,0 +1,309 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to player-controlled Spazzes."""
from __future__ import annotations
from typing import TYPE_CHECKING, TypeVar, overload
import ba
from bastd.actor.spaz import Spaz
from spazmod import modifyspaz
if TYPE_CHECKING:
from typing import Any, Sequence, Literal
# pylint: disable=invalid-name
PlayerType = TypeVar('PlayerType', bound=ba.Player)
TeamType = TypeVar('TeamType', bound=ba.Team)
# pylint: enable=invalid-name
class PlayerSpazHurtMessage:
"""A message saying a PlayerSpaz was hurt.
Category: **Message Classes**
"""
spaz: PlayerSpaz
"""The PlayerSpaz that was hurt"""
def __init__(self, spaz: PlayerSpaz):
"""Instantiate with the given ba.Spaz value."""
self.spaz = spaz
class PlayerSpaz(Spaz):
"""A Spaz subclass meant to be controlled by a ba.Player.
Category: **Gameplay Classes**
When a PlayerSpaz dies, it delivers a ba.PlayerDiedMessage
to the current ba.Activity. (unless the death was the result of the
player leaving the game, in which case no message is sent)
When a PlayerSpaz is hurt, it delivers a PlayerSpazHurtMessage
to the current ba.Activity.
"""
def __init__(
self,
player: ba.Player,
color: Sequence[float] = (1.0, 1.0, 1.0),
highlight: Sequence[float] = (0.5, 0.5, 0.5),
character: str = 'Spaz',
powerups_expire: bool = True,
):
"""Create a spaz for the provided ba.Player.
Note: this does not wire up any controls;
you must call connect_controls_to_player() to do so.
"""
character=modifyspaz.getCharacter(player,character)
super().__init__(
color=color,
highlight=highlight,
character=character,
source_player=player,
start_invincible=True,
powerups_expire=powerups_expire,
)
self.last_player_attacked_by: ba.Player | None = None
self.last_attacked_time = 0.0
self.last_attacked_type: tuple[str, str] | None = None
self.held_count = 0
self.last_player_held_by: ba.Player | None = None
self._player = player
self._drive_player_position()
import custom_hooks
custom_hooks.playerspaz_init(self, self.node, self._player)
# Overloads to tell the type system our return type based on doraise val.
@overload
def getplayer(
self, playertype: type[PlayerType], doraise: Literal[False] = False
) -> PlayerType | None:
...
@overload
def getplayer(
self, playertype: type[PlayerType], doraise: Literal[True]
) -> PlayerType:
...
def getplayer(
self, playertype: type[PlayerType], doraise: bool = False
) -> PlayerType | None:
"""Get the ba.Player associated with this Spaz.
By default this will return None if the Player no longer exists.
If you are logically certain that the Player still exists, pass
doraise=False to get a non-optional return type.
"""
player: Any = self._player
assert isinstance(player, playertype)
if not player.exists() and doraise:
raise ba.PlayerNotFoundError()
return player if player.exists() else None
def connect_controls_to_player(
self,
enable_jump: bool = True,
enable_punch: bool = True,
enable_pickup: bool = True,
enable_bomb: bool = True,
enable_run: bool = True,
enable_fly: bool = True,
) -> None:
"""Wire this spaz up to the provided ba.Player.
Full control of the character is given by default
but can be selectively limited by passing False
to specific arguments.
"""
player = self.getplayer(ba.Player)
assert player
# Reset any currently connected player and/or the player we're
# wiring up.
if self._connected_to_player:
if player != self._connected_to_player:
player.resetinput()
self.disconnect_controls_from_player()
else:
player.resetinput()
player.assigninput(ba.InputType.UP_DOWN, self.on_move_up_down)
player.assigninput(ba.InputType.LEFT_RIGHT, self.on_move_left_right)
player.assigninput(
ba.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press
)
player.assigninput(
ba.InputType.HOLD_POSITION_RELEASE, self.on_hold_position_release
)
intp = ba.InputType
if enable_jump:
player.assigninput(intp.JUMP_PRESS, self.on_jump_press)
player.assigninput(intp.JUMP_RELEASE, self.on_jump_release)
if enable_pickup:
player.assigninput(intp.PICK_UP_PRESS, self.on_pickup_press)
player.assigninput(intp.PICK_UP_RELEASE, self.on_pickup_release)
if enable_punch:
player.assigninput(intp.PUNCH_PRESS, self.on_punch_press)
player.assigninput(intp.PUNCH_RELEASE, self.on_punch_release)
if enable_bomb:
player.assigninput(intp.BOMB_PRESS, self.on_bomb_press)
player.assigninput(intp.BOMB_RELEASE, self.on_bomb_release)
if enable_run:
player.assigninput(intp.RUN, self.on_run)
if enable_fly:
player.assigninput(intp.FLY_PRESS, self.on_fly_press)
player.assigninput(intp.FLY_RELEASE, self.on_fly_release)
self._connected_to_player = player
def disconnect_controls_from_player(self) -> None:
"""
Completely sever any previously connected
ba.Player from control of this spaz.
"""
if self._connected_to_player:
self._connected_to_player.resetinput()
self._connected_to_player = None
# Send releases for anything in case its held.
self.on_move_up_down(0)
self.on_move_left_right(0)
self.on_hold_position_release()
self.on_jump_release()
self.on_pickup_release()
self.on_punch_release()
self.on_bomb_release()
self.on_run(0.0)
self.on_fly_release()
else:
print(
'WARNING: disconnect_controls_from_player() called for'
' non-connected player'
)
def handlemessage(self, msg: Any) -> Any:
# FIXME: Tidy this up.
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
# pylint: disable=too-many-nested-blocks
assert not self.expired
# Keep track of if we're being held and by who most recently.
if isinstance(msg, ba.PickedUpMessage):
# Augment standard behavior.
super().handlemessage(msg)
self.held_count += 1
picked_up_by = msg.node.source_player
if picked_up_by:
self.last_player_held_by = picked_up_by
elif isinstance(msg, ba.DroppedMessage):
# Augment standard behavior.
super().handlemessage(msg)
self.held_count -= 1
if self.held_count < 0:
print('ERROR: spaz held_count < 0')
# Let's count someone dropping us as an attack.
picked_up_by = msg.node.source_player
if picked_up_by:
self.last_player_attacked_by = picked_up_by
self.last_attacked_time = ba.time()
self.last_attacked_type = ('picked_up', 'default')
elif isinstance(msg, ba.StandMessage):
super().handlemessage(msg) # Augment standard behavior.
# Our Spaz was just moved somewhere. Explicitly update
# our associated player's position in case it is being used
# for logic (otherwise it will be out of date until next step)
self._drive_player_position()
elif isinstance(msg, ba.DieMessage):
# Report player deaths to the game.
if not self._dead:
# Immediate-mode or left-game deaths don't count as 'kills'.
killed = (
not msg.immediate and msg.how is not ba.DeathType.LEFT_GAME
)
activity = self._activity()
player = self.getplayer(ba.Player, False)
if not killed:
killerplayer = None
else:
# If this player was being held at the time of death,
# the holder is the killer.
if self.held_count > 0 and self.last_player_held_by:
killerplayer = self.last_player_held_by
else:
# Otherwise, if they were attacked by someone in the
# last few seconds, that person is the killer.
# Otherwise it was a suicide.
# FIXME: Currently disabling suicides in Co-Op since
# all bot kills would register as suicides; need to
# change this from last_player_attacked_by to
# something like last_actor_attacked_by to fix that.
if (
self.last_player_attacked_by
and ba.time() - self.last_attacked_time < 4.0
):
killerplayer = self.last_player_attacked_by
else:
# ok, call it a suicide unless we're in co-op
if activity is not None and not isinstance(
activity.session, ba.CoopSession
):
killerplayer = player
else:
killerplayer = None
# We should never wind up with a dead-reference here;
# we want to use None in that case.
assert killerplayer is None or killerplayer
# Only report if both the player and the activity still exist.
if killed and activity is not None and player:
activity.handlemessage(
ba.PlayerDiedMessage(
player, killed, killerplayer, msg.how
)
)
super().handlemessage(msg) # Augment standard behavior.
# Keep track of the player who last hit us for point rewarding.
elif isinstance(msg, ba.HitMessage):
source_player = msg.get_source_player(type(self._player))
if source_player:
self.last_player_attacked_by = source_player
self.last_attacked_time = ba.time()
self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
super().handlemessage(msg) # Augment standard behavior.
activity = self._activity()
if activity is not None and self._player.exists():
activity.handlemessage(PlayerSpazHurtMessage(self))
else:
return super().handlemessage(msg)
return None
def _drive_player_position(self) -> None:
"""Drive our ba.Player's official position
If our position is changed explicitly, this should be called again
to instantly update the player position (otherwise it would be out
of date until the next sim step)
"""
player = self._player
if player:
assert self.node
assert player.node
self.node.connectattr('torso_position', player.node, 'position')

View file

@ -0,0 +1,127 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence
class PopupText(ba.Actor):
"""Text that pops up above a position to denote something special.
category: Gameplay Classes
"""
def __init__(
self,
text: str | ba.Lstr,
position: Sequence[float] = (0.0, 0.0, 0.0),
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
random_offset: float = 0.5,
offset: Sequence[float] = (0.0, 0.0, 0.0),
scale: float = 1.0,
):
"""Instantiate with given values.
random_offset is the amount of random offset from the provided position
that will be applied. This can help multiple achievements from
overlapping too much.
"""
super().__init__()
if len(color) == 3:
color = (color[0], color[1], color[2], 1.0)
pos = (
position[0] + offset[0] + random_offset * (0.5 - random.random()),
position[1] + offset[1] + random_offset * (0.5 - random.random()),
position[2] + offset[2] + random_offset * (0.5 - random.random()),
)
self.node = ba.newnode(
'text',
attrs={
'text': text,
'in_world': True,
'shadow': 1.0,
'flatness': 1.0,
'h_align': 'center',
},
delegate=self,
)
lifespan = 1.5
# scale up
ba.animate(
self.node,
'scale',
{
0: 0.0,
lifespan * 0.11: 0.020 * 0.7 * scale,
lifespan * 0.16: 0.013 * 0.7 * scale,
lifespan * 0.25: 0.014 * 0.7 * scale,
},
)
# translate upward
self._tcombine = ba.newnode(
'combine',
owner=self.node,
attrs={'input0': pos[0], 'input2': pos[2], 'size': 3},
)
ba.animate(
self._tcombine, 'input1', {0: pos[1] + 1.5, lifespan: pos[1] + 2.0}
)
self._tcombine.connectattr('output', self.node, 'position')
# fade our opacity in/out
self._combine = ba.newnode(
'combine',
owner=self.node,
attrs={
'input0': color[0],
'input1': color[1],
'input2': color[2],
'size': 4,
},
)
for i in range(4):
ba.animate(
self._combine,
'input' + str(i),
{
0.13 * lifespan: color[i],
0.18 * lifespan: 4.0 * color[i],
0.22 * lifespan: color[i],
},
)
ba.animate(
self._combine,
'input3',
{
0: 0,
0.1 * lifespan: color[3],
0.7 * lifespan: color[3],
lifespan: 0,
},
)
self._combine.connectattr('output', self.node, 'color')
# kill ourself
self._die_timer = ba.Timer(
lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
else:
super().handlemessage(msg)

View file

@ -0,0 +1,319 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
DEFAULT_POWERUP_INTERVAL = 8.0
class _TouchedMessage:
pass
class PowerupBoxFactory:
"""A collection of media and other resources used by ba.Powerups.
Category: **Gameplay Classes**
A single instance of this is shared between all powerups
and can be retrieved via ba.Powerup.get_factory().
"""
model: ba.Model
"""The ba.Model of the powerup box."""
model_simple: ba.Model
"""A simpler ba.Model of the powerup box, for use in shadows, etc."""
tex_bomb: ba.Texture
"""Triple-bomb powerup ba.Texture."""
tex_punch: ba.Texture
"""Punch powerup ba.Texture."""
tex_ice_bombs: ba.Texture
"""Ice bomb powerup ba.Texture."""
tex_sticky_bombs: ba.Texture
"""Sticky bomb powerup ba.Texture."""
tex_shield: ba.Texture
"""Shield powerup ba.Texture."""
tex_impact_bombs: ba.Texture
"""Impact-bomb powerup ba.Texture."""
tex_health: ba.Texture
"""Health powerup ba.Texture."""
tex_land_mines: ba.Texture
"""Land-mine powerup ba.Texture."""
tex_curse: ba.Texture
"""Curse powerup ba.Texture."""
health_powerup_sound: ba.Sound
"""ba.Sound played when a health powerup is accepted."""
powerup_sound: ba.Sound
"""ba.Sound played when a powerup is accepted."""
powerdown_sound: ba.Sound
"""ba.Sound that can be used when powerups wear off."""
powerup_material: ba.Material
"""ba.Material applied to powerup boxes."""
powerup_accept_material: ba.Material
"""Powerups will send a ba.PowerupMessage to anything they touch
that has this ba.Material applied."""
_STORENAME = ba.storagename()
def __init__(self) -> None:
"""Instantiate a PowerupBoxFactory.
You shouldn't need to do this; call Powerup.get_factory()
to get a shared instance.
"""
from ba.internal import get_default_powerup_distribution
shared = SharedObjects.get()
self._lastpoweruptype: str | None = None
self.model = ba.getmodel('powerup')
self.model_simple = ba.getmodel('powerupSimple')
self.tex_bomb = ba.gettexture('powerupBomb')
self.tex_punch = ba.gettexture('powerupPunch')
self.tex_ice_bombs = ba.gettexture('powerupIceBombs')
self.tex_sticky_bombs = ba.gettexture('powerupStickyBombs')
self.tex_shield = ba.gettexture('powerupShield')
self.tex_impact_bombs = ba.gettexture('powerupImpactBombs')
self.tex_health = ba.gettexture('powerupHealth')
self.tex_land_mines = ba.gettexture('powerupLandMines')
self.tex_curse = ba.gettexture('powerupCurse')
self.health_powerup_sound = ba.getsound('healthPowerup')
self.powerup_sound = ba.getsound('powerup01')
self.powerdown_sound = ba.getsound('powerdown01')
self.drop_sound = ba.getsound('boxDrop')
# Material for powerups.
self.powerup_material = ba.Material()
# Material for anyone wanting to accept powerups.
self.powerup_accept_material = ba.Material()
# Pass a powerup-touched message to applicable stuff.
self.powerup_material.add_actions(
conditions=('they_have_material', self.powerup_accept_material),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', _TouchedMessage()),
),
)
# We don't wanna be picked up.
self.powerup_material.add_actions(
conditions=('they_have_material', shared.pickup_material),
actions=('modify_part_collision', 'collide', False),
)
self.powerup_material.add_actions(
conditions=('they_have_material', shared.footing_material),
actions=('impact_sound', self.drop_sound, 0.5, 0.1),
)
self._powerupdist: list[str] = []
for powerup, freq in get_default_powerup_distribution():
for _i in range(int(freq)):
self._powerupdist.append(powerup)
def get_random_powerup_type(
self,
forcetype: str | None = None,
excludetypes: list[str] | None = None,
) -> str:
"""Returns a random powerup type (string).
See ba.Powerup.poweruptype for available type values.
There are certain non-random aspects to this; a 'curse' powerup,
for instance, is always followed by a 'health' powerup (to keep things
interesting). Passing 'forcetype' forces a given returned type while
still properly interacting with the non-random aspects of the system
(ie: forcing a 'curse' powerup will result
in the next powerup being health).
"""
if excludetypes is None:
excludetypes = []
if forcetype:
ptype = forcetype
else:
# If the last one was a curse, make this one a health to
# provide some hope.
if self._lastpoweruptype == 'curse':
ptype = 'health'
else:
while True:
ptype = self._powerupdist[
random.randint(0, len(self._powerupdist) - 1)
]
if ptype not in excludetypes:
break
self._lastpoweruptype = ptype
return ptype
@classmethod
def get(cls) -> PowerupBoxFactory:
"""Return a shared ba.PowerupBoxFactory object, creating if needed."""
activity = ba.getactivity()
if activity is None:
raise ba.ContextError('No current activity.')
factory = activity.customdata.get(cls._STORENAME)
if factory is None:
factory = activity.customdata[cls._STORENAME] = PowerupBoxFactory()
assert isinstance(factory, PowerupBoxFactory)
return factory
class PowerupBox(ba.Actor):
"""A box that grants a powerup.
category: Gameplay Classes
This will deliver a ba.PowerupMessage to anything that touches it
which has the ba.PowerupBoxFactory.powerup_accept_material applied.
"""
poweruptype: str
"""The string powerup type. This can be 'triple_bombs', 'punch',
'ice_bombs', 'impact_bombs', 'land_mines', 'sticky_bombs', 'shield',
'health', or 'curse'."""
node: ba.Node
"""The 'prop' ba.Node representing this box."""
def __init__(
self,
position: Sequence[float] = (0.0, 1.0, 0.0),
poweruptype: str = 'triple_bombs',
expire: bool = True,
):
"""Create a powerup-box of the requested type at the given position.
see ba.Powerup.poweruptype for valid type strings.
"""
super().__init__()
shared = SharedObjects.get()
factory = PowerupBoxFactory.get()
self.poweruptype = poweruptype
self._powersgiven = False
if poweruptype == 'triple_bombs':
tex = factory.tex_bomb
elif poweruptype == 'punch':
tex = factory.tex_punch
elif poweruptype == 'ice_bombs':
tex = factory.tex_ice_bombs
elif poweruptype == 'impact_bombs':
tex = factory.tex_impact_bombs
elif poweruptype == 'land_mines':
tex = factory.tex_land_mines
elif poweruptype == 'sticky_bombs':
tex = factory.tex_sticky_bombs
elif poweruptype == 'shield':
tex = factory.tex_shield
elif poweruptype == 'health':
tex = factory.tex_health
elif poweruptype == 'curse':
tex = factory.tex_curse
else:
raise ValueError('invalid poweruptype: ' + str(poweruptype))
if len(position) != 3:
raise ValueError('expected 3 floats for position')
self.node = ba.newnode(
'prop',
delegate=self,
attrs={
'body': 'box',
'position': position,
'model': factory.model,
'light_model': factory.model_simple,
'shadow_size': 0.5,
'color_texture': tex,
'reflection': 'powerup',
'reflection_scale': [1.0],
'materials': (factory.powerup_material, shared.object_material),
},
) # yapf: disable
# Animate in.
curve = ba.animate(self.node, 'model_scale', {0: 0, 0.14: 1.6, 0.2: 1})
ba.timer(0.2, curve.delete)
if expire:
ba.timer(
DEFAULT_POWERUP_INTERVAL - 2.5,
ba.WeakCall(self._start_flashing),
)
ba.timer(
DEFAULT_POWERUP_INTERVAL - 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()),
)
def _start_flashing(self) -> None:
if self.node:
self.node.flashing = True
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.PowerupAcceptMessage):
factory = PowerupBoxFactory.get()
assert self.node
if self.poweruptype == 'health':
ba.playsound(
factory.health_powerup_sound, 3, position=self.node.position
)
ba.playsound(factory.powerup_sound, 3, position=self.node.position)
self._powersgiven = True
self.handlemessage(ba.DieMessage())
elif isinstance(msg, _TouchedMessage):
if not self._powersgiven:
node = ba.getcollision().opposingnode
node.handlemessage(
ba.PowerupMessage(self.poweruptype, sourcenode=self.node)
)
elif isinstance(msg, ba.DieMessage):
if self.node:
if msg.immediate:
self.node.delete()
else:
ba.animate(self.node, 'model_scale', {0: 1, 0.1: 0})
ba.timer(0.1, self.node.delete)
elif isinstance(msg, ba.OutOfBoundsMessage):
self.handlemessage(ba.DieMessage())
elif isinstance(msg, ba.HitMessage):
# Don't die on punches (that's annoying).
if msg.hit_type != 'punch':
self.handlemessage(ba.DieMessage())
else:
return super().handlemessage(msg)
return None

View file

@ -0,0 +1,168 @@
# Released under the MIT License. See LICENSE for details.
#
"""Implements respawn icon actor."""
from __future__ import annotations
import weakref
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
pass
class RespawnIcon:
"""An icon with a countdown that appears alongside the screen.
category: Gameplay Classes
This is used to indicate that a ba.Player is waiting to respawn.
"""
_MASKTEXSTORENAME = ba.storagename('masktex')
_ICONSSTORENAME = ba.storagename('icons')
def __init__(self, player: ba.Player, respawn_time: float):
"""Instantiate with a ba.Player and respawn_time (in seconds)."""
self._visible = True
on_right, offs_extra, respawn_icons = self._get_context(player)
# Cache our mask tex on the team for easy access.
mask_tex = player.team.customdata.get(self._MASKTEXSTORENAME)
if mask_tex is None:
mask_tex = ba.gettexture('characterIconMask')
player.team.customdata[self._MASKTEXSTORENAME] = mask_tex
assert isinstance(mask_tex, ba.Texture)
# Now find the first unused slot and use that.
index = 0
while (
index in respawn_icons
and respawn_icons[index]() is not None
and respawn_icons[index]().visible
):
index += 1
respawn_icons[index] = weakref.ref(self)
offs = offs_extra + index * -53
icon = player.get_icon()
texture = icon['texture']
h_offs = -10
ipos = (-40 - h_offs if on_right else 40 + h_offs, -180 + offs)
self._image: ba.NodeActor | None = ba.NodeActor(
ba.newnode(
'image',
attrs={
'texture': texture,
'tint_texture': icon['tint_texture'],
'tint_color': icon['tint_color'],
'tint2_color': icon['tint2_color'],
'mask_texture': mask_tex,
'position': ipos,
'scale': (32, 32),
'opacity': 1.0,
'absolute_scale': True,
'attach': 'topRight' if on_right else 'topLeft',
},
)
)
assert self._image.node
ba.animate(self._image.node, 'opacity', {0.0: 0, 0.2: 0.7})
npos = (-40 - h_offs if on_right else 40 + h_offs, -205 + 49 + offs)
self._name: ba.NodeActor | None = ba.NodeActor(
ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': 'right' if on_right else 'left',
'text': ba.Lstr(value=player.getname()),
'maxwidth': 100,
'h_align': 'center',
'v_align': 'center',
'shadow': 1.0,
'flatness': 1.0,
'color': ba.safecolor(icon['tint_color']),
'scale': 0.5,
'position': npos,
},
)
)
assert self._name.node
ba.animate(self._name.node, 'scale', {0: 0, 0.1: 0.5})
tpos = (-60 - h_offs if on_right else 60 + h_offs, -192 + offs)
self._text: ba.NodeActor | None = ba.NodeActor(
ba.newnode(
'text',
attrs={
'position': tpos,
'h_attach': 'right' if on_right else 'left',
'h_align': 'right' if on_right else 'left',
'scale': 0.9,
'shadow': 0.5,
'flatness': 0.5,
'v_attach': 'top',
'color': ba.safecolor(icon['tint_color']),
'text': '',
},
)
)
assert self._text.node
ba.animate(self._text.node, 'scale', {0: 0, 0.1: 0.9})
self._respawn_time = ba.time() + respawn_time
self._update()
self._timer: ba.Timer | None = ba.Timer(
1.0, ba.WeakCall(self._update), repeat=True
)
@property
def visible(self) -> bool:
"""Is this icon still visible?"""
return self._visible
def _get_context(self, player: ba.Player) -> tuple[bool, float, dict]:
"""Return info on where we should be shown and stored."""
activity = ba.getactivity()
if isinstance(ba.getsession(), ba.DualTeamSession):
on_right = player.team.id % 2 == 1
# Store a list of icons in the team.
icons = player.team.customdata.get(self._ICONSSTORENAME)
if icons is None:
player.team.customdata[self._ICONSSTORENAME] = icons = {}
assert isinstance(icons, dict)
offs_extra = -20
else:
on_right = False
# Store a list of icons in the activity.
icons = activity.customdata.get(self._ICONSSTORENAME)
if icons is None:
activity.customdata[self._ICONSSTORENAME] = icons = {}
assert isinstance(icons, dict)
if isinstance(activity.session, ba.FreeForAllSession):
offs_extra = -150
else:
offs_extra = -20
return on_right, offs_extra, icons
def _update(self) -> None:
remaining = int(round(self._respawn_time - ba.time()))
if remaining > 0:
assert self._text is not None
if self._text.node:
self._text.node.text = str(remaining)
else:
self._visible = False
self._image = self._text = self._timer = self._name = None

View file

@ -0,0 +1,447 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines ScoreBoard Actor and related functionality."""
from __future__ import annotations
import weakref
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence
class _Entry:
def __init__(
self,
scoreboard: Scoreboard,
team: ba.Team,
do_cover: bool,
scale: float,
label: ba.Lstr | None,
flash_length: float,
):
# pylint: disable=too-many-statements
self._scoreboard = weakref.ref(scoreboard)
self._do_cover = do_cover
self._scale = scale
self._flash_length = flash_length
self._width = 140.0 * self._scale
self._height = 32.0 * self._scale
self._bar_width = 2.0 * self._scale
self._bar_height = 32.0 * self._scale
self._bar_tex = self._backing_tex = ba.gettexture('bar')
self._cover_tex = ba.gettexture('uiAtlas')
self._model = ba.getmodel('meterTransparent')
self._pos: Sequence[float] | None = None
self._flash_timer: ba.Timer | None = None
self._flash_counter: int | None = None
self._flash_colors: bool | None = None
self._score: float | None = None
safe_team_color = ba.safecolor(team.color, target_intensity=1.0)
# FIXME: Should not do things conditionally for vr-mode, as there may
# be non-vr clients connected which will also get these value.
vrmode = ba.app.vr_mode
if self._do_cover:
if vrmode:
self._backing_color = [0.1 + c * 0.1 for c in safe_team_color]
else:
self._backing_color = [0.05 + c * 0.17 for c in safe_team_color]
else:
self._backing_color = [0.05 + c * 0.1 for c in safe_team_color]
opacity = (0.8 if vrmode else 0.8) if self._do_cover else 0.5
self._backing = ba.NodeActor(
ba.newnode(
'image',
attrs={
'scale': (self._width, self._height),
'opacity': opacity,
'color': self._backing_color,
'vr_depth': -3,
'attach': 'topLeft',
'texture': self._backing_tex,
},
)
)
self._barcolor = safe_team_color
self._bar = ba.NodeActor(
ba.newnode(
'image',
attrs={
'opacity': 0.7,
'color': self._barcolor,
'attach': 'topLeft',
'texture': self._bar_tex,
},
)
)
self._bar_scale = ba.newnode(
'combine',
owner=self._bar.node,
attrs={
'size': 2,
'input0': self._bar_width,
'input1': self._bar_height,
},
)
assert self._bar.node
self._bar_scale.connectattr('output', self._bar.node, 'scale')
self._bar_position = ba.newnode(
'combine',
owner=self._bar.node,
attrs={'size': 2, 'input0': 0, 'input1': 0},
)
self._bar_position.connectattr('output', self._bar.node, 'position')
self._cover_color = safe_team_color
if self._do_cover:
self._cover = ba.NodeActor(
ba.newnode(
'image',
attrs={
'scale': (self._width * 1.15, self._height * 1.6),
'opacity': 1.0,
'color': self._cover_color,
'vr_depth': 2,
'attach': 'topLeft',
'texture': self._cover_tex,
'model_transparent': self._model,
},
)
)
clr = safe_team_color
maxwidth = 130.0 * (1.0 - scoreboard.score_split)
flatness = (1.0 if vrmode else 0.5) if self._do_cover else 1.0
self._score_text = ba.NodeActor(
ba.newnode(
'text',
attrs={
'h_attach': 'left',
'v_attach': 'top',
'h_align': 'right',
'v_align': 'center',
'maxwidth': maxwidth,
'vr_depth': 2,
'scale': self._scale * 0.9,
'text': '',
'shadow': 1.0 if vrmode else 0.5,
'flatness': flatness,
'color': clr,
},
)
)
clr = safe_team_color
team_name_label: str | ba.Lstr
if label is not None:
team_name_label = label
else:
team_name_label = team.name
# We do our own clipping here; should probably try to tap into some
# existing functionality.
if isinstance(team_name_label, ba.Lstr):
# Hmmm; if the team-name is a non-translatable value lets go
# ahead and clip it otherwise we leave it as-is so
# translation can occur..
if team_name_label.is_flat_value():
val = team_name_label.evaluate()
if len(val) > 10:
team_name_label = ba.Lstr(value=val[:10] + '...')
else:
if len(team_name_label) > 10:
team_name_label = team_name_label[:10] + '...'
team_name_label = ba.Lstr(value=team_name_label)
flatness = (1.0 if vrmode else 0.5) if self._do_cover else 1.0
self._name_text = ba.NodeActor(
ba.newnode(
'text',
attrs={
'h_attach': 'left',
'v_attach': 'top',
'h_align': 'left',
'v_align': 'center',
'vr_depth': 2,
'scale': self._scale * 0.9,
'shadow': 1.0 if vrmode else 0.5,
'flatness': flatness,
'maxwidth': 130 * scoreboard.score_split,
'text': team_name_label,
'color': clr + (1.0,),
},
)
)
def flash(self, countdown: bool, extra_flash: bool) -> None:
"""Flash momentarily."""
self._flash_timer = ba.Timer(
0.1, ba.WeakCall(self._do_flash), repeat=True
)
if countdown:
self._flash_counter = 10
else:
self._flash_counter = int(20.0 * self._flash_length)
if extra_flash:
self._flash_counter *= 4
self._set_flash_colors(True)
def set_position(self, position: Sequence[float]) -> None:
"""Set the entry's position."""
# Abort if we've been killed
if not self._backing.node:
return
self._pos = tuple(position)
self._backing.node.position = (
position[0] + self._width / 2,
position[1] - self._height / 2,
)
if self._do_cover:
assert self._cover.node
self._cover.node.position = (
position[0] + self._width / 2,
position[1] - self._height / 2,
)
self._bar_position.input0 = self._pos[0] + self._bar_width / 2
self._bar_position.input1 = self._pos[1] - self._bar_height / 2
assert self._score_text.node
self._score_text.node.position = (
self._pos[0] + self._width - 7.0 * self._scale,
self._pos[1] - self._bar_height + 16.0 * self._scale,
)
assert self._name_text.node
self._name_text.node.position = (
self._pos[0] + 7.0 * self._scale,
self._pos[1] - self._bar_height + 16.0 * self._scale,
)
def _set_flash_colors(self, flash: bool) -> None:
self._flash_colors = flash
def _safesetcolor(node: ba.Node | None, val: Any) -> None:
if node:
node.color = val
if flash:
scale = 2.0
_safesetcolor(
self._backing.node,
(
self._backing_color[0] * scale,
self._backing_color[1] * scale,
self._backing_color[2] * scale,
),
)
_safesetcolor(
self._bar.node,
(
self._barcolor[0] * scale,
self._barcolor[1] * scale,
self._barcolor[2] * scale,
),
)
if self._do_cover:
_safesetcolor(
self._cover.node,
(
self._cover_color[0] * scale,
self._cover_color[1] * scale,
self._cover_color[2] * scale,
),
)
else:
_safesetcolor(self._backing.node, self._backing_color)
_safesetcolor(self._bar.node, self._barcolor)
if self._do_cover:
_safesetcolor(self._cover.node, self._cover_color)
def _do_flash(self) -> None:
assert self._flash_counter is not None
if self._flash_counter <= 0:
self._set_flash_colors(False)
else:
self._flash_counter -= 1
self._set_flash_colors(not self._flash_colors)
def set_value(
self,
score: float,
max_score: float | None = None,
countdown: bool = False,
flash: bool = True,
show_value: bool = True,
) -> None:
"""Set the value for the scoreboard entry."""
# If we have no score yet, just set it.. otherwise compare
# and see if we should flash.
if self._score is None:
self._score = score
else:
if score > self._score or (countdown and score < self._score):
extra_flash = (
max_score is not None
and score >= max_score
and not countdown
) or (countdown and score == 0)
if flash:
self.flash(countdown, extra_flash)
self._score = score
if max_score is None:
self._bar_width = 0.0
else:
if countdown:
self._bar_width = max(
2.0 * self._scale,
self._width * (1.0 - (float(score) / max_score)),
)
else:
self._bar_width = max(
2.0 * self._scale,
self._width * (min(1.0, float(score) / max_score)),
)
cur_width = self._bar_scale.input0
ba.animate(
self._bar_scale, 'input0', {0.0: cur_width, 0.25: self._bar_width}
)
self._bar_scale.input1 = self._bar_height
cur_x = self._bar_position.input0
assert self._pos is not None
ba.animate(
self._bar_position,
'input0',
{0.0: cur_x, 0.25: self._pos[0] + self._bar_width / 2},
)
self._bar_position.input1 = self._pos[1] - self._bar_height / 2
assert self._score_text.node
if show_value:
self._score_text.node.text = str(score)
else:
self._score_text.node.text = ''
class _EntryProxy:
"""Encapsulates adding/removing of a scoreboard Entry."""
def __init__(self, scoreboard: Scoreboard, team: ba.Team):
self._scoreboard = weakref.ref(scoreboard)
# Have to store ID here instead of a weak-ref since the team will be
# dead when we die and need to remove it.
self._team_id = team.id
def __del__(self) -> None:
scoreboard = self._scoreboard()
# Remove our team from the scoreboard if its still around.
# (but deferred, in case we die in a sim step or something where
# its illegal to modify nodes)
if scoreboard is None:
return
try:
ba.pushcall(ba.Call(scoreboard.remove_team, self._team_id))
except ba.ContextError:
# This happens if we fire after the activity expires.
# In that case we don't need to do anything.
pass
class Scoreboard:
"""A display for player or team scores during a game.
category: Gameplay Classes
"""
_ENTRYSTORENAME = ba.storagename('entry')
def __init__(self, label: ba.Lstr | None = None, score_split: float = 0.7):
"""Instantiate a scoreboard.
Label can be something like 'points' and will
show up on boards if provided.
"""
self._flat_tex = ba.gettexture('null')
self._entries: dict[int, _Entry] = {}
self._label = label
self.score_split = score_split
# For free-for-all we go simpler since we have one per player.
self._pos: Sequence[float]
if isinstance(ba.getsession(), ba.FreeForAllSession):
self._do_cover = False
self._spacing = 35.0
self._pos = (17.0, -65.0)
self._scale = 0.8
self._flash_length = 0.5
else:
self._do_cover = True
self._spacing = 50.0
self._pos = (20.0, -70.0)
self._scale = 1.0
self._flash_length = 1.0
def set_team_value(
self,
team: ba.Team,
score: float,
max_score: float | None = None,
countdown: bool = False,
flash: bool = True,
show_value: bool = True,
) -> None:
"""Update the score-board display for the given ba.Team."""
if team.id not in self._entries:
self._add_team(team)
# Create a proxy in the team which will kill
# our entry when it dies (for convenience)
assert self._ENTRYSTORENAME not in team.customdata
team.customdata[self._ENTRYSTORENAME] = _EntryProxy(self, team)
# Now set the entry.
self._entries[team.id].set_value(
score=score,
max_score=max_score,
countdown=countdown,
flash=flash,
show_value=show_value,
)
def _add_team(self, team: ba.Team) -> None:
if team.id in self._entries:
raise RuntimeError('Duplicate team add')
self._entries[team.id] = _Entry(
self,
team,
do_cover=self._do_cover,
scale=self._scale,
label=self._label,
flash_length=self._flash_length,
)
self._update_teams()
def remove_team(self, team_id: int) -> None:
"""Remove the team with the given id from the scoreboard."""
del self._entries[team_id]
self._update_teams()
def _update_teams(self) -> None:
pos = list(self._pos)
for entry in list(self._entries.values()):
entry.set_position(pos)
pos[1] -= self._spacing * self._scale

View file

@ -0,0 +1,118 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines some lovely Actor(s)."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence, Callable
# FIXME: Should make this an Actor.
class Spawner:
"""Utility for delayed spawning of objects.
Category: **Gameplay Classes**
Creates a light flash and sends a Spawner.SpawnMessage
to the current activity after a delay.
"""
class SpawnMessage:
"""Spawn message sent by a Spawner after its delay has passed.
Category: **Message Classes**
"""
spawner: Spawner
"""The ba.Spawner we came from."""
data: Any
"""The data object passed by the user."""
pt: Sequence[float]
"""The spawn position."""
def __init__(
self,
spawner: Spawner,
data: Any,
pt: Sequence[float], # pylint: disable=invalid-name
):
"""Instantiate with the given values."""
self.spawner = spawner
self.data = data
self.pt = pt # pylint: disable=invalid-name
def __init__(
self,
data: Any = None,
pt: Sequence[float] = (0, 0, 0), # pylint: disable=invalid-name
spawn_time: float = 1.0,
send_spawn_message: bool = True,
spawn_callback: Callable[[], Any] | None = None,
):
"""Instantiate a Spawner.
Requires some custom data, a position,
and a spawn-time in seconds.
"""
self._spawn_callback = spawn_callback
self._send_spawn_message = send_spawn_message
self._spawner_sound = ba.getsound('swip2')
self._data = data
self._pt = pt
# create a light where the spawn will happen
self._light = ba.newnode(
'light',
attrs={
'position': tuple(pt),
'radius': 0.1,
'color': (1.0, 0.1, 0.1),
'lights_volumes': False,
},
)
scl = float(spawn_time) / 3.75
min_val = 0.4
max_val = 0.7
ba.playsound(self._spawner_sound, position=self._light.position)
ba.animate(
self._light,
'intensity',
{
0.0: 0.0,
0.25 * scl: max_val,
0.500 * scl: min_val,
0.750 * scl: max_val,
1.000 * scl: min_val,
1.250 * scl: 1.1 * max_val,
1.500 * scl: min_val,
1.750 * scl: 1.2 * max_val,
2.000 * scl: min_val,
2.250 * scl: 1.3 * max_val,
2.500 * scl: min_val,
2.750 * scl: 1.4 * max_val,
3.000 * scl: min_val,
3.250 * scl: 1.5 * max_val,
3.500 * scl: min_val,
3.750 * scl: 2.0,
4.000 * scl: 0.0,
},
)
ba.timer(spawn_time, self._spawn)
def _spawn(self) -> None:
ba.timer(1.0, self._light.delete)
if self._spawn_callback is not None:
self._spawn_callback()
if self._send_spawn_message:
# only run if our activity still exists
activity = ba.getactivity()
if activity is not None:
activity.handlemessage(
self.SpawnMessage(self, self._data, self._pt)
)

1633
dist/ba_data/python/bastd/actor/spaz.py vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,988 @@
# Released under the MIT License. See LICENSE for details.
#
"""Appearance functionality for spazzes."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
pass
def get_appearances(include_locked: bool = False) -> list[str]:
"""Get the list of available spaz appearances."""
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
get_purchased = ba.internal.get_purchased
disallowed = []
if not include_locked:
# hmm yeah this'll be tough to hack...
if not get_purchased('characters.santa'):
disallowed.append('Santa Claus')
if not get_purchased('characters.frosty'):
disallowed.append('Frosty')
if not get_purchased('characters.bones'):
disallowed.append('Bones')
if not get_purchased('characters.bernard'):
disallowed.append('Bernard')
if not get_purchased('characters.pixie'):
disallowed.append('Pixel')
if not get_purchased('characters.pascal'):
disallowed.append('Pascal')
if not get_purchased('characters.actionhero'):
disallowed.append('Todd McBurton')
if not get_purchased('characters.taobaomascot'):
disallowed.append('Taobao Mascot')
if not get_purchased('characters.agent'):
disallowed.append('Agent Johnson')
if not get_purchased('characters.jumpsuit'):
disallowed.append('Lee')
if not get_purchased('characters.assassin'):
disallowed.append('Zola')
if not get_purchased('characters.wizard'):
disallowed.append('Grumbledorf')
if not get_purchased('characters.cowboy'):
disallowed.append('Butch')
if not get_purchased('characters.witch'):
disallowed.append('Witch')
if not get_purchased('characters.warrior'):
disallowed.append('Warrior')
if not get_purchased('characters.superhero'):
disallowed.append('Middle-Man')
if not get_purchased('characters.alien'):
disallowed.append('Alien')
if not get_purchased('characters.oldlady'):
disallowed.append('OldLady')
if not get_purchased('characters.gladiator'):
disallowed.append('Gladiator')
if not get_purchased('characters.wrestler'):
disallowed.append('Wrestler')
if not get_purchased('characters.operasinger'):
disallowed.append('Gretel')
if not get_purchased('characters.robot'):
disallowed.append('Robot')
if not get_purchased('characters.cyborg'):
disallowed.append('B-9000')
if not get_purchased('characters.bunny'):
disallowed.append('Easter Bunny')
if not get_purchased('characters.kronk'):
disallowed.append('Kronk')
if not get_purchased('characters.zoe'):
disallowed.append('Zoe')
if not get_purchased('characters.jackmorgan'):
disallowed.append('Jack Morgan')
if not get_purchased('characters.mel'):
disallowed.append('Mel')
if not get_purchased('characters.snakeshadow'):
disallowed.append('Snake Shadow')
return [
s for s in list(ba.app.spaz_appearances.keys()) if s not in disallowed
]
class Appearance:
"""Create and fill out one of these suckers to define a spaz appearance"""
def __init__(self, name: str):
self.name = name
if self.name in ba.app.spaz_appearances:
raise Exception(
'spaz appearance name "' + self.name + '" already exists.'
)
ba.app.spaz_appearances[self.name] = self
self.color_texture = ''
self.color_mask_texture = ''
self.icon_texture = ''
self.icon_mask_texture = ''
self.head_model = ''
self.torso_model = ''
self.pelvis_model = ''
self.upper_arm_model = ''
self.forearm_model = ''
self.hand_model = ''
self.upper_leg_model = ''
self.lower_leg_model = ''
self.toes_model = ''
self.jump_sounds: list[str] = []
self.attack_sounds: list[str] = []
self.impact_sounds: list[str] = []
self.death_sounds: list[str] = []
self.pickup_sounds: list[str] = []
self.fall_sounds: list[str] = []
self.style = 'spaz'
self.default_color: tuple[float, float, float] | None = None
self.default_highlight: tuple[float, float, float] | None = None
def register_appearances() -> None:
"""Register our builtin spaz appearances."""
# this is quite ugly but will be going away so not worth cleaning up
# pylint: disable=invalid-name
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
# Spaz #######################################
t = Appearance('Spaz')
t.color_texture = 'neoSpazColor'
t.color_mask_texture = 'neoSpazColorMask'
t.icon_texture = 'neoSpazIcon'
t.icon_mask_texture = 'neoSpazIconColorMask'
t.head_model = 'neoSpazHead'
t.torso_model = 'neoSpazTorso'
t.pelvis_model = 'neoSpazPelvis'
t.upper_arm_model = 'neoSpazUpperArm'
t.forearm_model = 'neoSpazForeArm'
t.hand_model = 'neoSpazHand'
t.upper_leg_model = 'neoSpazUpperLeg'
t.lower_leg_model = 'neoSpazLowerLeg'
t.toes_model = 'neoSpazToes'
t.jump_sounds = ['spazJump01', 'spazJump02', 'spazJump03', 'spazJump04']
t.attack_sounds = [
'spazAttack01',
'spazAttack02',
'spazAttack03',
'spazAttack04',
]
t.impact_sounds = [
'spazImpact01',
'spazImpact02',
'spazImpact03',
'spazImpact04',
]
t.death_sounds = ['spazDeath01']
t.pickup_sounds = ['spazPickup01']
t.fall_sounds = ['spazFall01']
t.style = 'spaz'
# Zoe #####################################
t = Appearance('Zoe')
t.color_texture = 'zoeColor'
t.color_mask_texture = 'zoeColorMask'
t.default_color = (0.6, 0.6, 0.6)
t.default_highlight = (0, 1, 0)
t.icon_texture = 'zoeIcon'
t.icon_mask_texture = 'zoeIconColorMask'
t.head_model = 'zoeHead'
t.torso_model = 'zoeTorso'
t.pelvis_model = 'zoePelvis'
t.upper_arm_model = 'zoeUpperArm'
t.forearm_model = 'zoeForeArm'
t.hand_model = 'zoeHand'
t.upper_leg_model = 'zoeUpperLeg'
t.lower_leg_model = 'zoeLowerLeg'
t.toes_model = 'zoeToes'
t.jump_sounds = ['zoeJump01', 'zoeJump02', 'zoeJump03']
t.attack_sounds = [
'zoeAttack01',
'zoeAttack02',
'zoeAttack03',
'zoeAttack04',
]
t.impact_sounds = [
'zoeImpact01',
'zoeImpact02',
'zoeImpact03',
'zoeImpact04',
]
t.death_sounds = ['zoeDeath01']
t.pickup_sounds = ['zoePickup01']
t.fall_sounds = ['zoeFall01']
t.style = 'female'
# Ninja ##########################################
t = Appearance('Snake Shadow')
t.color_texture = 'ninjaColor'
t.color_mask_texture = 'ninjaColorMask'
t.default_color = (1, 1, 1)
t.default_highlight = (0.55, 0.8, 0.55)
t.icon_texture = 'ninjaIcon'
t.icon_mask_texture = 'ninjaIconColorMask'
t.head_model = 'ninjaHead'
t.torso_model = 'ninjaTorso'
t.pelvis_model = 'ninjaPelvis'
t.upper_arm_model = 'ninjaUpperArm'
t.forearm_model = 'ninjaForeArm'
t.hand_model = 'ninjaHand'
t.upper_leg_model = 'ninjaUpperLeg'
t.lower_leg_model = 'ninjaLowerLeg'
t.toes_model = 'ninjaToes'
ninja_attacks = ['ninjaAttack' + str(i + 1) + '' for i in range(7)]
ninja_hits = ['ninjaHit' + str(i + 1) + '' for i in range(8)]
ninja_jumps = ['ninjaAttack' + str(i + 1) + '' for i in range(7)]
t.jump_sounds = ninja_jumps
t.attack_sounds = ninja_attacks
t.impact_sounds = ninja_hits
t.death_sounds = ['ninjaDeath1']
t.pickup_sounds = ninja_attacks
t.fall_sounds = ['ninjaFall1']
t.style = 'ninja'
# Barbarian #####################################
t = Appearance('Kronk')
t.color_texture = 'kronk'
t.color_mask_texture = 'kronkColorMask'
t.default_color = (0.4, 0.5, 0.4)
t.default_highlight = (1, 0.5, 0.3)
t.icon_texture = 'kronkIcon'
t.icon_mask_texture = 'kronkIconColorMask'
t.head_model = 'kronkHead'
t.torso_model = 'kronkTorso'
t.pelvis_model = 'kronkPelvis'
t.upper_arm_model = 'kronkUpperArm'
t.forearm_model = 'kronkForeArm'
t.hand_model = 'kronkHand'
t.upper_leg_model = 'kronkUpperLeg'
t.lower_leg_model = 'kronkLowerLeg'
t.toes_model = 'kronkToes'
kronk_sounds = [
'kronk1',
'kronk2',
'kronk3',
'kronk4',
'kronk5',
'kronk6',
'kronk7',
'kronk8',
'kronk9',
'kronk10',
]
t.jump_sounds = kronk_sounds
t.attack_sounds = kronk_sounds
t.impact_sounds = kronk_sounds
t.death_sounds = ['kronkDeath']
t.pickup_sounds = kronk_sounds
t.fall_sounds = ['kronkFall']
t.style = 'kronk'
# Chef ###########################################
t = Appearance('Mel')
t.color_texture = 'melColor'
t.color_mask_texture = 'melColorMask'
t.default_color = (1, 1, 1)
t.default_highlight = (0.1, 0.6, 0.1)
t.icon_texture = 'melIcon'
t.icon_mask_texture = 'melIconColorMask'
t.head_model = 'melHead'
t.torso_model = 'melTorso'
t.pelvis_model = 'kronkPelvis'
t.upper_arm_model = 'melUpperArm'
t.forearm_model = 'melForeArm'
t.hand_model = 'melHand'
t.upper_leg_model = 'melUpperLeg'
t.lower_leg_model = 'melLowerLeg'
t.toes_model = 'melToes'
mel_sounds = [
'mel01',
'mel02',
'mel03',
'mel04',
'mel05',
'mel06',
'mel07',
'mel08',
'mel09',
'mel10',
]
t.attack_sounds = mel_sounds
t.jump_sounds = mel_sounds
t.impact_sounds = mel_sounds
t.death_sounds = ['melDeath01']
t.pickup_sounds = mel_sounds
t.fall_sounds = ['melFall01']
t.style = 'mel'
# Pirate #######################################
t = Appearance('Jack Morgan')
t.color_texture = 'jackColor'
t.color_mask_texture = 'jackColorMask'
t.default_color = (1, 0.2, 0.1)
t.default_highlight = (1, 1, 0)
t.icon_texture = 'jackIcon'
t.icon_mask_texture = 'jackIconColorMask'
t.head_model = 'jackHead'
t.torso_model = 'jackTorso'
t.pelvis_model = 'kronkPelvis'
t.upper_arm_model = 'jackUpperArm'
t.forearm_model = 'jackForeArm'
t.hand_model = 'jackHand'
t.upper_leg_model = 'jackUpperLeg'
t.lower_leg_model = 'jackLowerLeg'
t.toes_model = 'jackToes'
hit_sounds = [
'jackHit01',
'jackHit02',
'jackHit03',
'jackHit04',
'jackHit05',
'jackHit06',
'jackHit07',
]
sounds = ['jack01', 'jack02', 'jack03', 'jack04', 'jack05', 'jack06']
t.attack_sounds = sounds
t.jump_sounds = sounds
t.impact_sounds = hit_sounds
t.death_sounds = ['jackDeath01']
t.pickup_sounds = sounds
t.fall_sounds = ['jackFall01']
t.style = 'pirate'
# Santa ######################################
t = Appearance('Santa Claus')
t.color_texture = 'santaColor'
t.color_mask_texture = 'santaColorMask'
t.default_color = (1, 0, 0)
t.default_highlight = (1, 1, 1)
t.icon_texture = 'santaIcon'
t.icon_mask_texture = 'santaIconColorMask'
t.head_model = 'santaHead'
t.torso_model = 'santaTorso'
t.pelvis_model = 'kronkPelvis'
t.upper_arm_model = 'santaUpperArm'
t.forearm_model = 'santaForeArm'
t.hand_model = 'santaHand'
t.upper_leg_model = 'santaUpperLeg'
t.lower_leg_model = 'santaLowerLeg'
t.toes_model = 'santaToes'
hit_sounds = ['santaHit01', 'santaHit02', 'santaHit03', 'santaHit04']
sounds = ['santa01', 'santa02', 'santa03', 'santa04', 'santa05']
t.attack_sounds = sounds
t.jump_sounds = sounds
t.impact_sounds = hit_sounds
t.death_sounds = ['santaDeath']
t.pickup_sounds = sounds
t.fall_sounds = ['santaFall']
t.style = 'santa'
# Snowman ###################################
t = Appearance('Frosty')
t.color_texture = 'frostyColor'
t.color_mask_texture = 'frostyColorMask'
t.default_color = (0.5, 0.5, 1)
t.default_highlight = (1, 0.5, 0)
t.icon_texture = 'frostyIcon'
t.icon_mask_texture = 'frostyIconColorMask'
t.head_model = 'frostyHead'
t.torso_model = 'frostyTorso'
t.pelvis_model = 'frostyPelvis'
t.upper_arm_model = 'frostyUpperArm'
t.forearm_model = 'frostyForeArm'
t.hand_model = 'frostyHand'
t.upper_leg_model = 'frostyUpperLeg'
t.lower_leg_model = 'frostyLowerLeg'
t.toes_model = 'frostyToes'
frosty_sounds = ['frosty01', 'frosty02', 'frosty03', 'frosty04', 'frosty05']
frosty_hit_sounds = ['frostyHit01', 'frostyHit02', 'frostyHit03']
t.attack_sounds = frosty_sounds
t.jump_sounds = frosty_sounds
t.impact_sounds = frosty_hit_sounds
t.death_sounds = ['frostyDeath']
t.pickup_sounds = frosty_sounds
t.fall_sounds = ['frostyFall']
t.style = 'frosty'
# Skeleton ################################
t = Appearance('Bones')
t.color_texture = 'bonesColor'
t.color_mask_texture = 'bonesColorMask'
t.default_color = (0.6, 0.9, 1)
t.default_highlight = (0.6, 0.9, 1)
t.icon_texture = 'bonesIcon'
t.icon_mask_texture = 'bonesIconColorMask'
t.head_model = 'bonesHead'
t.torso_model = 'bonesTorso'
t.pelvis_model = 'bonesPelvis'
t.upper_arm_model = 'bonesUpperArm'
t.forearm_model = 'bonesForeArm'
t.hand_model = 'bonesHand'
t.upper_leg_model = 'bonesUpperLeg'
t.lower_leg_model = 'bonesLowerLeg'
t.toes_model = 'bonesToes'
bones_sounds = ['bones1', 'bones2', 'bones3']
bones_hit_sounds = ['bones1', 'bones2', 'bones3']
t.attack_sounds = bones_sounds
t.jump_sounds = bones_sounds
t.impact_sounds = bones_hit_sounds
t.death_sounds = ['bonesDeath']
t.pickup_sounds = bones_sounds
t.fall_sounds = ['bonesFall']
t.style = 'bones'
# Bear ###################################
t = Appearance('Bernard')
t.color_texture = 'bearColor'
t.color_mask_texture = 'bearColorMask'
t.default_color = (0.7, 0.5, 0.0)
t.icon_texture = 'bearIcon'
t.icon_mask_texture = 'bearIconColorMask'
t.head_model = 'bearHead'
t.torso_model = 'bearTorso'
t.pelvis_model = 'bearPelvis'
t.upper_arm_model = 'bearUpperArm'
t.forearm_model = 'bearForeArm'
t.hand_model = 'bearHand'
t.upper_leg_model = 'bearUpperLeg'
t.lower_leg_model = 'bearLowerLeg'
t.toes_model = 'bearToes'
bear_sounds = ['bear1', 'bear2', 'bear3', 'bear4']
bear_hit_sounds = ['bearHit1', 'bearHit2']
t.attack_sounds = bear_sounds
t.jump_sounds = bear_sounds
t.impact_sounds = bear_hit_sounds
t.death_sounds = ['bearDeath']
t.pickup_sounds = bear_sounds
t.fall_sounds = ['bearFall']
t.style = 'bear'
# Penguin ###################################
t = Appearance('Pascal')
t.color_texture = 'penguinColor'
t.color_mask_texture = 'penguinColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'penguinIcon'
t.icon_mask_texture = 'penguinIconColorMask'
t.head_model = 'penguinHead'
t.torso_model = 'penguinTorso'
t.pelvis_model = 'penguinPelvis'
t.upper_arm_model = 'penguinUpperArm'
t.forearm_model = 'penguinForeArm'
t.hand_model = 'penguinHand'
t.upper_leg_model = 'penguinUpperLeg'
t.lower_leg_model = 'penguinLowerLeg'
t.toes_model = 'penguinToes'
penguin_sounds = ['penguin1', 'penguin2', 'penguin3', 'penguin4']
penguin_hit_sounds = ['penguinHit1', 'penguinHit2']
t.attack_sounds = penguin_sounds
t.jump_sounds = penguin_sounds
t.impact_sounds = penguin_hit_sounds
t.death_sounds = ['penguinDeath']
t.pickup_sounds = penguin_sounds
t.fall_sounds = ['penguinFall']
t.style = 'penguin'
# Ali ###################################
t = Appearance('Taobao Mascot')
t.color_texture = 'aliColor'
t.color_mask_texture = 'aliColorMask'
t.default_color = (1, 0.5, 0)
t.default_highlight = (1, 1, 1)
t.icon_texture = 'aliIcon'
t.icon_mask_texture = 'aliIconColorMask'
t.head_model = 'aliHead'
t.torso_model = 'aliTorso'
t.pelvis_model = 'aliPelvis'
t.upper_arm_model = 'aliUpperArm'
t.forearm_model = 'aliForeArm'
t.hand_model = 'aliHand'
t.upper_leg_model = 'aliUpperLeg'
t.lower_leg_model = 'aliLowerLeg'
t.toes_model = 'aliToes'
ali_sounds = ['ali1', 'ali2', 'ali3', 'ali4']
ali_hit_sounds = ['aliHit1', 'aliHit2']
t.attack_sounds = ali_sounds
t.jump_sounds = ali_sounds
t.impact_sounds = ali_hit_sounds
t.death_sounds = ['aliDeath']
t.pickup_sounds = ali_sounds
t.fall_sounds = ['aliFall']
t.style = 'ali'
# cyborg ###################################
t = Appearance('B-9000')
t.color_texture = 'cyborgColor'
t.color_mask_texture = 'cyborgColorMask'
t.default_color = (0.5, 0.5, 0.5)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'cyborgIcon'
t.icon_mask_texture = 'cyborgIconColorMask'
t.head_model = 'cyborgHead'
t.torso_model = 'cyborgTorso'
t.pelvis_model = 'cyborgPelvis'
t.upper_arm_model = 'cyborgUpperArm'
t.forearm_model = 'cyborgForeArm'
t.hand_model = 'cyborgHand'
t.upper_leg_model = 'cyborgUpperLeg'
t.lower_leg_model = 'cyborgLowerLeg'
t.toes_model = 'cyborgToes'
cyborg_sounds = ['cyborg1', 'cyborg2', 'cyborg3', 'cyborg4']
cyborg_hit_sounds = ['cyborgHit1', 'cyborgHit2']
t.attack_sounds = cyborg_sounds
t.jump_sounds = cyborg_sounds
t.impact_sounds = cyborg_hit_sounds
t.death_sounds = ['cyborgDeath']
t.pickup_sounds = cyborg_sounds
t.fall_sounds = ['cyborgFall']
t.style = 'cyborg'
# Agent ###################################
t = Appearance('Agent Johnson')
t.color_texture = 'agentColor'
t.color_mask_texture = 'agentColorMask'
t.default_color = (0.3, 0.3, 0.33)
t.default_highlight = (1, 0.5, 0.3)
t.icon_texture = 'agentIcon'
t.icon_mask_texture = 'agentIconColorMask'
t.head_model = 'agentHead'
t.torso_model = 'agentTorso'
t.pelvis_model = 'agentPelvis'
t.upper_arm_model = 'agentUpperArm'
t.forearm_model = 'agentForeArm'
t.hand_model = 'agentHand'
t.upper_leg_model = 'agentUpperLeg'
t.lower_leg_model = 'agentLowerLeg'
t.toes_model = 'agentToes'
agent_sounds = ['agent1', 'agent2', 'agent3', 'agent4']
agent_hit_sounds = ['agentHit1', 'agentHit2']
t.attack_sounds = agent_sounds
t.jump_sounds = agent_sounds
t.impact_sounds = agent_hit_sounds
t.death_sounds = ['agentDeath']
t.pickup_sounds = agent_sounds
t.fall_sounds = ['agentFall']
t.style = 'agent'
# Jumpsuit ###################################
t = Appearance('Lee')
t.color_texture = 'jumpsuitColor'
t.color_mask_texture = 'jumpsuitColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'jumpsuitIcon'
t.icon_mask_texture = 'jumpsuitIconColorMask'
t.head_model = 'jumpsuitHead'
t.torso_model = 'jumpsuitTorso'
t.pelvis_model = 'jumpsuitPelvis'
t.upper_arm_model = 'jumpsuitUpperArm'
t.forearm_model = 'jumpsuitForeArm'
t.hand_model = 'jumpsuitHand'
t.upper_leg_model = 'jumpsuitUpperLeg'
t.lower_leg_model = 'jumpsuitLowerLeg'
t.toes_model = 'jumpsuitToes'
jumpsuit_sounds = ['jumpsuit1', 'jumpsuit2', 'jumpsuit3', 'jumpsuit4']
jumpsuit_hit_sounds = ['jumpsuitHit1', 'jumpsuitHit2']
t.attack_sounds = jumpsuit_sounds
t.jump_sounds = jumpsuit_sounds
t.impact_sounds = jumpsuit_hit_sounds
t.death_sounds = ['jumpsuitDeath']
t.pickup_sounds = jumpsuit_sounds
t.fall_sounds = ['jumpsuitFall']
t.style = 'spaz'
# ActionHero ###################################
t = Appearance('Todd McBurton')
t.color_texture = 'actionHeroColor'
t.color_mask_texture = 'actionHeroColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'actionHeroIcon'
t.icon_mask_texture = 'actionHeroIconColorMask'
t.head_model = 'actionHeroHead'
t.torso_model = 'actionHeroTorso'
t.pelvis_model = 'actionHeroPelvis'
t.upper_arm_model = 'actionHeroUpperArm'
t.forearm_model = 'actionHeroForeArm'
t.hand_model = 'actionHeroHand'
t.upper_leg_model = 'actionHeroUpperLeg'
t.lower_leg_model = 'actionHeroLowerLeg'
t.toes_model = 'actionHeroToes'
action_hero_sounds = [
'actionHero1',
'actionHero2',
'actionHero3',
'actionHero4',
]
action_hero_hit_sounds = ['actionHeroHit1', 'actionHeroHit2']
t.attack_sounds = action_hero_sounds
t.jump_sounds = action_hero_sounds
t.impact_sounds = action_hero_hit_sounds
t.death_sounds = ['actionHeroDeath']
t.pickup_sounds = action_hero_sounds
t.fall_sounds = ['actionHeroFall']
t.style = 'spaz'
# Assassin ###################################
t = Appearance('Zola')
t.color_texture = 'assassinColor'
t.color_mask_texture = 'assassinColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'assassinIcon'
t.icon_mask_texture = 'assassinIconColorMask'
t.head_model = 'assassinHead'
t.torso_model = 'assassinTorso'
t.pelvis_model = 'assassinPelvis'
t.upper_arm_model = 'assassinUpperArm'
t.forearm_model = 'assassinForeArm'
t.hand_model = 'assassinHand'
t.upper_leg_model = 'assassinUpperLeg'
t.lower_leg_model = 'assassinLowerLeg'
t.toes_model = 'assassinToes'
assassin_sounds = ['assassin1', 'assassin2', 'assassin3', 'assassin4']
assassin_hit_sounds = ['assassinHit1', 'assassinHit2']
t.attack_sounds = assassin_sounds
t.jump_sounds = assassin_sounds
t.impact_sounds = assassin_hit_sounds
t.death_sounds = ['assassinDeath']
t.pickup_sounds = assassin_sounds
t.fall_sounds = ['assassinFall']
t.style = 'spaz'
# Wizard ###################################
t = Appearance('Grumbledorf')
t.color_texture = 'wizardColor'
t.color_mask_texture = 'wizardColorMask'
t.default_color = (0.2, 0.4, 1.0)
t.default_highlight = (0.06, 0.15, 0.4)
t.icon_texture = 'wizardIcon'
t.icon_mask_texture = 'wizardIconColorMask'
t.head_model = 'wizardHead'
t.torso_model = 'wizardTorso'
t.pelvis_model = 'wizardPelvis'
t.upper_arm_model = 'wizardUpperArm'
t.forearm_model = 'wizardForeArm'
t.hand_model = 'wizardHand'
t.upper_leg_model = 'wizardUpperLeg'
t.lower_leg_model = 'wizardLowerLeg'
t.toes_model = 'wizardToes'
wizard_sounds = ['wizard1', 'wizard2', 'wizard3', 'wizard4']
wizard_hit_sounds = ['wizardHit1', 'wizardHit2']
t.attack_sounds = wizard_sounds
t.jump_sounds = wizard_sounds
t.impact_sounds = wizard_hit_sounds
t.death_sounds = ['wizardDeath']
t.pickup_sounds = wizard_sounds
t.fall_sounds = ['wizardFall']
t.style = 'spaz'
# Cowboy ###################################
t = Appearance('Butch')
t.color_texture = 'cowboyColor'
t.color_mask_texture = 'cowboyColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'cowboyIcon'
t.icon_mask_texture = 'cowboyIconColorMask'
t.head_model = 'cowboyHead'
t.torso_model = 'cowboyTorso'
t.pelvis_model = 'cowboyPelvis'
t.upper_arm_model = 'cowboyUpperArm'
t.forearm_model = 'cowboyForeArm'
t.hand_model = 'cowboyHand'
t.upper_leg_model = 'cowboyUpperLeg'
t.lower_leg_model = 'cowboyLowerLeg'
t.toes_model = 'cowboyToes'
cowboy_sounds = ['cowboy1', 'cowboy2', 'cowboy3', 'cowboy4']
cowboy_hit_sounds = ['cowboyHit1', 'cowboyHit2']
t.attack_sounds = cowboy_sounds
t.jump_sounds = cowboy_sounds
t.impact_sounds = cowboy_hit_sounds
t.death_sounds = ['cowboyDeath']
t.pickup_sounds = cowboy_sounds
t.fall_sounds = ['cowboyFall']
t.style = 'spaz'
# Witch ###################################
t = Appearance('Witch')
t.color_texture = 'witchColor'
t.color_mask_texture = 'witchColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'witchIcon'
t.icon_mask_texture = 'witchIconColorMask'
t.head_model = 'witchHead'
t.torso_model = 'witchTorso'
t.pelvis_model = 'witchPelvis'
t.upper_arm_model = 'witchUpperArm'
t.forearm_model = 'witchForeArm'
t.hand_model = 'witchHand'
t.upper_leg_model = 'witchUpperLeg'
t.lower_leg_model = 'witchLowerLeg'
t.toes_model = 'witchToes'
witch_sounds = ['witch1', 'witch2', 'witch3', 'witch4']
witch_hit_sounds = ['witchHit1', 'witchHit2']
t.attack_sounds = witch_sounds
t.jump_sounds = witch_sounds
t.impact_sounds = witch_hit_sounds
t.death_sounds = ['witchDeath']
t.pickup_sounds = witch_sounds
t.fall_sounds = ['witchFall']
t.style = 'spaz'
# Warrior ###################################
t = Appearance('Warrior')
t.color_texture = 'warriorColor'
t.color_mask_texture = 'warriorColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'warriorIcon'
t.icon_mask_texture = 'warriorIconColorMask'
t.head_model = 'warriorHead'
t.torso_model = 'warriorTorso'
t.pelvis_model = 'warriorPelvis'
t.upper_arm_model = 'warriorUpperArm'
t.forearm_model = 'warriorForeArm'
t.hand_model = 'warriorHand'
t.upper_leg_model = 'warriorUpperLeg'
t.lower_leg_model = 'warriorLowerLeg'
t.toes_model = 'warriorToes'
warrior_sounds = ['warrior1', 'warrior2', 'warrior3', 'warrior4']
warrior_hit_sounds = ['warriorHit1', 'warriorHit2']
t.attack_sounds = warrior_sounds
t.jump_sounds = warrior_sounds
t.impact_sounds = warrior_hit_sounds
t.death_sounds = ['warriorDeath']
t.pickup_sounds = warrior_sounds
t.fall_sounds = ['warriorFall']
t.style = 'spaz'
# Superhero ###################################
t = Appearance('Middle-Man')
t.color_texture = 'superheroColor'
t.color_mask_texture = 'superheroColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'superheroIcon'
t.icon_mask_texture = 'superheroIconColorMask'
t.head_model = 'superheroHead'
t.torso_model = 'superheroTorso'
t.pelvis_model = 'superheroPelvis'
t.upper_arm_model = 'superheroUpperArm'
t.forearm_model = 'superheroForeArm'
t.hand_model = 'superheroHand'
t.upper_leg_model = 'superheroUpperLeg'
t.lower_leg_model = 'superheroLowerLeg'
t.toes_model = 'superheroToes'
superhero_sounds = ['superhero1', 'superhero2', 'superhero3', 'superhero4']
superhero_hit_sounds = ['superheroHit1', 'superheroHit2']
t.attack_sounds = superhero_sounds
t.jump_sounds = superhero_sounds
t.impact_sounds = superhero_hit_sounds
t.death_sounds = ['superheroDeath']
t.pickup_sounds = superhero_sounds
t.fall_sounds = ['superheroFall']
t.style = 'spaz'
# Alien ###################################
t = Appearance('Alien')
t.color_texture = 'alienColor'
t.color_mask_texture = 'alienColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'alienIcon'
t.icon_mask_texture = 'alienIconColorMask'
t.head_model = 'alienHead'
t.torso_model = 'alienTorso'
t.pelvis_model = 'alienPelvis'
t.upper_arm_model = 'alienUpperArm'
t.forearm_model = 'alienForeArm'
t.hand_model = 'alienHand'
t.upper_leg_model = 'alienUpperLeg'
t.lower_leg_model = 'alienLowerLeg'
t.toes_model = 'alienToes'
alien_sounds = ['alien1', 'alien2', 'alien3', 'alien4']
alien_hit_sounds = ['alienHit1', 'alienHit2']
t.attack_sounds = alien_sounds
t.jump_sounds = alien_sounds
t.impact_sounds = alien_hit_sounds
t.death_sounds = ['alienDeath']
t.pickup_sounds = alien_sounds
t.fall_sounds = ['alienFall']
t.style = 'spaz'
# OldLady ###################################
t = Appearance('OldLady')
t.color_texture = 'oldLadyColor'
t.color_mask_texture = 'oldLadyColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'oldLadyIcon'
t.icon_mask_texture = 'oldLadyIconColorMask'
t.head_model = 'oldLadyHead'
t.torso_model = 'oldLadyTorso'
t.pelvis_model = 'oldLadyPelvis'
t.upper_arm_model = 'oldLadyUpperArm'
t.forearm_model = 'oldLadyForeArm'
t.hand_model = 'oldLadyHand'
t.upper_leg_model = 'oldLadyUpperLeg'
t.lower_leg_model = 'oldLadyLowerLeg'
t.toes_model = 'oldLadyToes'
old_lady_sounds = ['oldLady1', 'oldLady2', 'oldLady3', 'oldLady4']
old_lady_hit_sounds = ['oldLadyHit1', 'oldLadyHit2']
t.attack_sounds = old_lady_sounds
t.jump_sounds = old_lady_sounds
t.impact_sounds = old_lady_hit_sounds
t.death_sounds = ['oldLadyDeath']
t.pickup_sounds = old_lady_sounds
t.fall_sounds = ['oldLadyFall']
t.style = 'spaz'
# Gladiator ###################################
t = Appearance('Gladiator')
t.color_texture = 'gladiatorColor'
t.color_mask_texture = 'gladiatorColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'gladiatorIcon'
t.icon_mask_texture = 'gladiatorIconColorMask'
t.head_model = 'gladiatorHead'
t.torso_model = 'gladiatorTorso'
t.pelvis_model = 'gladiatorPelvis'
t.upper_arm_model = 'gladiatorUpperArm'
t.forearm_model = 'gladiatorForeArm'
t.hand_model = 'gladiatorHand'
t.upper_leg_model = 'gladiatorUpperLeg'
t.lower_leg_model = 'gladiatorLowerLeg'
t.toes_model = 'gladiatorToes'
gladiator_sounds = ['gladiator1', 'gladiator2', 'gladiator3', 'gladiator4']
gladiator_hit_sounds = ['gladiatorHit1', 'gladiatorHit2']
t.attack_sounds = gladiator_sounds
t.jump_sounds = gladiator_sounds
t.impact_sounds = gladiator_hit_sounds
t.death_sounds = ['gladiatorDeath']
t.pickup_sounds = gladiator_sounds
t.fall_sounds = ['gladiatorFall']
t.style = 'spaz'
# Wrestler ###################################
t = Appearance('Wrestler')
t.color_texture = 'wrestlerColor'
t.color_mask_texture = 'wrestlerColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'wrestlerIcon'
t.icon_mask_texture = 'wrestlerIconColorMask'
t.head_model = 'wrestlerHead'
t.torso_model = 'wrestlerTorso'
t.pelvis_model = 'wrestlerPelvis'
t.upper_arm_model = 'wrestlerUpperArm'
t.forearm_model = 'wrestlerForeArm'
t.hand_model = 'wrestlerHand'
t.upper_leg_model = 'wrestlerUpperLeg'
t.lower_leg_model = 'wrestlerLowerLeg'
t.toes_model = 'wrestlerToes'
wrestler_sounds = ['wrestler1', 'wrestler2', 'wrestler3', 'wrestler4']
wrestler_hit_sounds = ['wrestlerHit1', 'wrestlerHit2']
t.attack_sounds = wrestler_sounds
t.jump_sounds = wrestler_sounds
t.impact_sounds = wrestler_hit_sounds
t.death_sounds = ['wrestlerDeath']
t.pickup_sounds = wrestler_sounds
t.fall_sounds = ['wrestlerFall']
t.style = 'spaz'
# OperaSinger ###################################
t = Appearance('Gretel')
t.color_texture = 'operaSingerColor'
t.color_mask_texture = 'operaSingerColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'operaSingerIcon'
t.icon_mask_texture = 'operaSingerIconColorMask'
t.head_model = 'operaSingerHead'
t.torso_model = 'operaSingerTorso'
t.pelvis_model = 'operaSingerPelvis'
t.upper_arm_model = 'operaSingerUpperArm'
t.forearm_model = 'operaSingerForeArm'
t.hand_model = 'operaSingerHand'
t.upper_leg_model = 'operaSingerUpperLeg'
t.lower_leg_model = 'operaSingerLowerLeg'
t.toes_model = 'operaSingerToes'
opera_singer_sounds = [
'operaSinger1',
'operaSinger2',
'operaSinger3',
'operaSinger4',
]
opera_singer_hit_sounds = ['operaSingerHit1', 'operaSingerHit2']
t.attack_sounds = opera_singer_sounds
t.jump_sounds = opera_singer_sounds
t.impact_sounds = opera_singer_hit_sounds
t.death_sounds = ['operaSingerDeath']
t.pickup_sounds = opera_singer_sounds
t.fall_sounds = ['operaSingerFall']
t.style = 'spaz'
# Pixie ###################################
t = Appearance('Pixel')
t.color_texture = 'pixieColor'
t.color_mask_texture = 'pixieColorMask'
t.default_color = (0, 1, 0.7)
t.default_highlight = (0.65, 0.35, 0.75)
t.icon_texture = 'pixieIcon'
t.icon_mask_texture = 'pixieIconColorMask'
t.head_model = 'pixieHead'
t.torso_model = 'pixieTorso'
t.pelvis_model = 'pixiePelvis'
t.upper_arm_model = 'pixieUpperArm'
t.forearm_model = 'pixieForeArm'
t.hand_model = 'pixieHand'
t.upper_leg_model = 'pixieUpperLeg'
t.lower_leg_model = 'pixieLowerLeg'
t.toes_model = 'pixieToes'
pixie_sounds = ['pixie1', 'pixie2', 'pixie3', 'pixie4']
pixie_hit_sounds = ['pixieHit1', 'pixieHit2']
t.attack_sounds = pixie_sounds
t.jump_sounds = pixie_sounds
t.impact_sounds = pixie_hit_sounds
t.death_sounds = ['pixieDeath']
t.pickup_sounds = pixie_sounds
t.fall_sounds = ['pixieFall']
t.style = 'pixie'
# Robot ###################################
t = Appearance('Robot')
t.color_texture = 'robotColor'
t.color_mask_texture = 'robotColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'robotIcon'
t.icon_mask_texture = 'robotIconColorMask'
t.head_model = 'robotHead'
t.torso_model = 'robotTorso'
t.pelvis_model = 'robotPelvis'
t.upper_arm_model = 'robotUpperArm'
t.forearm_model = 'robotForeArm'
t.hand_model = 'robotHand'
t.upper_leg_model = 'robotUpperLeg'
t.lower_leg_model = 'robotLowerLeg'
t.toes_model = 'robotToes'
robot_sounds = ['robot1', 'robot2', 'robot3', 'robot4']
robot_hit_sounds = ['robotHit1', 'robotHit2']
t.attack_sounds = robot_sounds
t.jump_sounds = robot_sounds
t.impact_sounds = robot_hit_sounds
t.death_sounds = ['robotDeath']
t.pickup_sounds = robot_sounds
t.fall_sounds = ['robotFall']
t.style = 'spaz'
# Bunny ###################################
t = Appearance('Easter Bunny')
t.color_texture = 'bunnyColor'
t.color_mask_texture = 'bunnyColorMask'
t.default_color = (1, 1, 1)
t.default_highlight = (1, 0.5, 0.5)
t.icon_texture = 'bunnyIcon'
t.icon_mask_texture = 'bunnyIconColorMask'
t.head_model = 'bunnyHead'
t.torso_model = 'bunnyTorso'
t.pelvis_model = 'bunnyPelvis'
t.upper_arm_model = 'bunnyUpperArm'
t.forearm_model = 'bunnyForeArm'
t.hand_model = 'bunnyHand'
t.upper_leg_model = 'bunnyUpperLeg'
t.lower_leg_model = 'bunnyLowerLeg'
t.toes_model = 'bunnyToes'
bunny_sounds = ['bunny1', 'bunny2', 'bunny3', 'bunny4']
bunny_hit_sounds = ['bunnyHit1', 'bunnyHit2']
t.attack_sounds = bunny_sounds
t.jump_sounds = ['bunnyJump']
t.impact_sounds = bunny_hit_sounds
t.death_sounds = ['bunnyDeath']
t.pickup_sounds = bunny_sounds
t.fall_sounds = ['bunnyFall']
t.style = 'bunny'

1120
dist/ba_data/python/bastd/actor/spazbot.py vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,307 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides a factory object from creating Spazzes."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
class SpazFactory:
"""Wraps up media and other resources used by ba.Spaz instances.
Category: **Gameplay Classes**
Generally one of these is created per ba.Activity and shared
between all spaz instances. Use ba.Spaz.get_factory() to return
the shared factory for the current activity.
"""
impact_sounds_medium: Sequence[ba.Sound]
"""A tuple of ba.Sound-s for when a ba.Spaz hits something kinda hard."""
impact_sounds_hard: Sequence[ba.Sound]
"""A tuple of ba.Sound-s for when a ba.Spaz hits something really hard."""
impact_sounds_harder: Sequence[ba.Sound]
"""A tuple of ba.Sound-s for when a ba.Spaz hits something really
really hard."""
single_player_death_sound: ba.Sound
"""The sound that plays for an 'important' spaz death such as in
co-op games."""
punch_sound_weak: ba.Sound
"""A weak punch ba.Sound."""
punch_sound: ba.Sound
"""A standard punch ba.Sound."""
punch_sound_strong: Sequence[ba.Sound]
"""A tuple of stronger sounding punch ba.Sounds."""
punch_sound_stronger: ba.Sound
"""A really really strong sounding punch ba.Sound."""
swish_sound: ba.Sound
"""A punch swish ba.Sound."""
block_sound: ba.Sound
"""A ba.Sound for when an attack is blocked by invincibility."""
shatter_sound: ba.Sound
"""A ba.Sound for when a frozen ba.Spaz shatters."""
splatter_sound: ba.Sound
"""A ba.Sound for when a ba.Spaz blows up via curse."""
spaz_material: ba.Material
"""A ba.Material applied to all of parts of a ba.Spaz."""
roller_material: ba.Material
"""A ba.Material applied to the invisible roller ball body that
a ba.Spaz uses for locomotion."""
punch_material: ba.Material
"""A ba.Material applied to the 'fist' of a ba.Spaz."""
pickup_material: ba.Material
"""A ba.Material applied to the 'grabber' body of a ba.Spaz."""
curse_material: ba.Material
"""A ba.Material applied to a cursed ba.Spaz that triggers an explosion."""
_STORENAME = ba.storagename()
def _preload(self, character: str) -> None:
"""Preload media needed for a given character."""
self.get_media(character)
def __init__(self) -> None:
"""Instantiate a factory object."""
# pylint: disable=cyclic-import
# FIXME: should probably put these somewhere common so we don't
# have to import them from a module that imports us.
from bastd.actor.spaz import (
PickupMessage,
PunchHitMessage,
CurseExplodeMessage,
)
shared = SharedObjects.get()
self.impact_sounds_medium = (
ba.getsound('impactMedium'),
ba.getsound('impactMedium2'),
)
self.impact_sounds_hard = (
ba.getsound('impactHard'),
ba.getsound('impactHard2'),
ba.getsound('impactHard3'),
)
self.impact_sounds_harder = (
ba.getsound('bigImpact'),
ba.getsound('bigImpact2'),
)
self.single_player_death_sound = ba.getsound('playerDeath')
self.punch_sound_weak = ba.getsound('punchWeak01')
self.punch_sound = ba.getsound('punch01')
self.punch_sound_strong = (
ba.getsound('punchStrong01'),
ba.getsound('punchStrong02'),
)
self.punch_sound_stronger = ba.getsound('superPunch')
self.swish_sound = ba.getsound('punchSwish')
self.block_sound = ba.getsound('block')
self.shatter_sound = ba.getsound('shatter')
self.splatter_sound = ba.getsound('splatter')
self.spaz_material = ba.Material()
self.roller_material = ba.Material()
self.punch_material = ba.Material()
self.pickup_material = ba.Material()
self.curse_material = ba.Material()
footing_material = shared.footing_material
object_material = shared.object_material
player_material = shared.player_material
region_material = shared.region_material
# Send footing messages to spazzes so they know when they're on
# solid ground.
# Eww; this probably should just be built into the spaz node.
self.roller_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1),
),
)
self.spaz_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1),
),
)
# Punches.
self.punch_material.add_actions(
conditions=('they_are_different_node_than_us',),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', PunchHitMessage()),
),
)
# Pickups.
self.pickup_material.add_actions(
conditions=(
('they_are_different_node_than_us',),
'and',
('they_have_material', object_material),
),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', PickupMessage()),
),
)
# Curse.
self.curse_material.add_actions(
conditions=(
('they_are_different_node_than_us',),
'and',
('they_have_material', player_material),
),
actions=(
'message',
'our_node',
'at_connect',
CurseExplodeMessage(),
),
)
self.foot_impact_sounds = (
ba.getsound('footImpact01'),
ba.getsound('footImpact02'),
ba.getsound('footImpact03'),
)
self.foot_skid_sound = ba.getsound('skid01')
self.foot_roll_sound = ba.getsound('scamper01')
self.roller_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('impact_sound', self.foot_impact_sounds, 1, 0.2),
('skid_sound', self.foot_skid_sound, 20, 0.3),
('roll_sound', self.foot_roll_sound, 20, 3.0),
),
)
self.skid_sound = ba.getsound('gravelSkid')
self.spaz_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('impact_sound', self.foot_impact_sounds, 20, 6),
('skid_sound', self.skid_sound, 2.0, 1),
('roll_sound', self.skid_sound, 2.0, 1),
),
)
self.shield_up_sound = ba.getsound('shieldUp')
self.shield_down_sound = ba.getsound('shieldDown')
self.shield_hit_sound = ba.getsound('shieldHit')
# We don't want to collide with stuff we're initially overlapping
# (unless its marked with a special region material).
self.spaz_material.add_actions(
conditions=(
(
('we_are_younger_than', 51),
'and',
('they_are_different_node_than_us',),
),
'and',
('they_dont_have_material', region_material),
),
actions=('modify_node_collision', 'collide', False),
)
self.spaz_media: dict[str, Any] = {}
# Lets load some basic rules.
# (allows them to be tweaked from the master server)
self.shield_decay_rate = ba.internal.get_v1_account_misc_read_val(
'rsdr', 10.0
)
self.punch_cooldown = ba.internal.get_v1_account_misc_read_val(
'rpc', 400
)
self.punch_cooldown_gloves = ba.internal.get_v1_account_misc_read_val(
'rpcg', 300
)
self.punch_power_scale = ba.internal.get_v1_account_misc_read_val(
'rpp', 1.2
)
self.punch_power_scale_gloves = (
ba.internal.get_v1_account_misc_read_val('rppg', 1.4)
)
self.max_shield_spillover_damage = (
ba.internal.get_v1_account_misc_read_val('rsms', 500)
)
def get_style(self, character: str) -> str:
"""Return the named style for this character.
(this influences subtle aspects of their appearance, etc)
"""
return ba.app.spaz_appearances[character].style
def get_media(self, character: str) -> dict[str, Any]:
"""Return the set of media used by this variant of spaz."""
char = ba.app.spaz_appearances[character]
if character not in self.spaz_media:
media = self.spaz_media[character] = {
'jump_sounds': [ba.getsound(s) for s in char.jump_sounds],
'attack_sounds': [ba.getsound(s) for s in char.attack_sounds],
'impact_sounds': [ba.getsound(s) for s in char.impact_sounds],
'death_sounds': [ba.getsound(s) for s in char.death_sounds],
'pickup_sounds': [ba.getsound(s) for s in char.pickup_sounds],
'fall_sounds': [ba.getsound(s) for s in char.fall_sounds],
'color_texture': ba.gettexture(char.color_texture),
'color_mask_texture': ba.gettexture(char.color_mask_texture),
'head_model': ba.getmodel(char.head_model),
'torso_model': ba.getmodel(char.torso_model),
'pelvis_model': ba.getmodel(char.pelvis_model),
'upper_arm_model': ba.getmodel(char.upper_arm_model),
'forearm_model': ba.getmodel(char.forearm_model),
'hand_model': ba.getmodel(char.hand_model),
'upper_leg_model': ba.getmodel(char.upper_leg_model),
'lower_leg_model': ba.getmodel(char.lower_leg_model),
'toes_model': ba.getmodel(char.toes_model),
}
else:
media = self.spaz_media[character]
return media
@classmethod
def get(cls) -> SpazFactory:
"""Return the shared ba.SpazFactory, creating it if necessary."""
# pylint: disable=cyclic-import
activity = ba.getactivity()
factory = activity.customdata.get(cls._STORENAME)
if factory is None:
factory = activity.customdata[cls._STORENAME] = SpazFactory()
assert isinstance(factory, SpazFactory)
return factory

230
dist/ba_data/python/bastd/actor/text.py vendored Normal file
View file

@ -0,0 +1,230 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence
class Text(ba.Actor):
"""Text with some tricks."""
class Transition(Enum):
"""Transition types for text."""
FADE_IN = 'fade_in'
IN_RIGHT = 'in_right'
IN_LEFT = 'in_left'
IN_BOTTOM = 'in_bottom'
IN_BOTTOM_SLOW = 'in_bottom_slow'
IN_TOP_SLOW = 'in_top_slow'
class HAlign(Enum):
"""Horizontal alignment type."""
LEFT = 'left'
CENTER = 'center'
RIGHT = 'right'
class VAlign(Enum):
"""Vertical alignment type."""
NONE = 'none'
CENTER = 'center'
class HAttach(Enum):
"""Horizontal attach type."""
LEFT = 'left'
CENTER = 'center'
RIGHT = 'right'
class VAttach(Enum):
"""Vertical attach type."""
BOTTOM = 'bottom'
CENTER = 'center'
TOP = 'top'
def __init__(
self,
text: str | ba.Lstr,
position: tuple[float, float] = (0.0, 0.0),
h_align: HAlign = HAlign.LEFT,
v_align: VAlign = VAlign.NONE,
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
transition: Transition | None = None,
transition_delay: float = 0.0,
flash: bool = False,
v_attach: VAttach = VAttach.CENTER,
h_attach: HAttach = HAttach.CENTER,
scale: float = 1.0,
transition_out_delay: float | None = None,
maxwidth: float | None = None,
shadow: float = 0.5,
flatness: float = 0.0,
vr_depth: float = 0.0,
host_only: bool = False,
front: bool = False,
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
super().__init__()
self.node = ba.newnode(
'text',
delegate=self,
attrs={
'text': text,
'color': color,
'position': position,
'h_align': h_align.value,
'vr_depth': vr_depth,
'v_align': v_align.value,
'h_attach': h_attach.value,
'v_attach': v_attach.value,
'shadow': shadow,
'flatness': flatness,
'maxwidth': 0.0 if maxwidth is None else maxwidth,
'host_only': host_only,
'front': front,
'scale': scale,
},
)
if transition is self.Transition.FADE_IN:
if flash:
raise RuntimeError(
'fixme: flash and fade-in currently cant both be on'
)
cmb = ba.newnode(
'combine',
owner=self.node,
attrs={
'input0': color[0],
'input1': color[1],
'input2': color[2],
'size': 4,
},
)
keys = {transition_delay: 0.0, transition_delay + 0.5: color[3]}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = color[3]
keys[transition_delay + transition_out_delay + 0.5] = 0.0
ba.animate(cmb, 'input3', keys)
cmb.connectattr('output', self.node, 'color')
if flash:
mult = 2.0
tm1 = 0.15
tm2 = 0.3
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 4})
ba.animate(
cmb,
'input0',
{0.0: color[0] * mult, tm1: color[0], tm2: color[0] * mult},
loop=True,
)
ba.animate(
cmb,
'input1',
{0.0: color[1] * mult, tm1: color[1], tm2: color[1] * mult},
loop=True,
)
ba.animate(
cmb,
'input2',
{0.0: color[2] * mult, tm1: color[2], tm2: color[2] * mult},
loop=True,
)
cmb.input3 = color[3]
cmb.connectattr('output', self.node, 'color')
cmb = self.position_combine = ba.newnode(
'combine', owner=self.node, attrs={'size': 2}
)
if transition is self.Transition.IN_RIGHT:
keys = {
transition_delay: position[0] + 1300,
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
ba.animate(cmb, 'input0', keys)
cmb.input1 = position[1]
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_LEFT:
keys = {
transition_delay: position[0] - 1300,
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = position[0]
keys[transition_delay + transition_out_delay + 0.2] = (
position[0] - 1300.0
)
o_keys[transition_delay + transition_out_delay + 0.15] = 1.0
o_keys[transition_delay + transition_out_delay + 0.2] = 0.0
ba.animate(cmb, 'input0', keys)
cmb.input1 = position[1]
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_BOTTOM_SLOW:
keys = {
transition_delay: -100.0,
transition_delay + 1.0: position[1],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.2: 1.0}
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_BOTTOM:
keys = {
transition_delay: -100.0,
transition_delay + 0.2: position[1],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = position[1]
keys[transition_delay + transition_out_delay + 0.2] = -100.0
o_keys[transition_delay + transition_out_delay + 0.15] = 1.0
o_keys[transition_delay + transition_out_delay + 0.2] = 0.0
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_TOP_SLOW:
keys = {
transition_delay: 400.0,
transition_delay + 3.5: position[1],
}
o_keys = {transition_delay: 0, transition_delay + 1.0: 1.0}
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
else:
assert transition is self.Transition.FADE_IN or transition is None
cmb.input0 = position[0]
cmb.input1 = position[1]
cmb.connectattr('output', self.node, 'position')
# If we're transitioning out, die at the end of it.
if transition_out_delay is not None:
ba.timer(
transition_delay + transition_out_delay + 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()),
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
return None
return super().handlemessage(msg)

View file

@ -0,0 +1,101 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides tip related Actor(s)."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any
class TipsText(ba.Actor):
"""A bit of text showing various helpful game tips."""
def __init__(self, offs_y: float = 100.0):
super().__init__()
self._tip_scale = 0.8
self._tip_title_scale = 1.1
self._offs_y = offs_y
self.node = ba.newnode(
'text',
delegate=self,
attrs={
'text': '',
'scale': self._tip_scale,
'h_align': 'left',
'maxwidth': 800,
'vr_depth': -20,
'v_align': 'center',
'v_attach': 'bottom',
},
)
tval = ba.Lstr(
value='${A}:', subs=[('${A}', ba.Lstr(resource='tipText'))]
)
self.title_node = ba.newnode(
'text',
delegate=self,
attrs={
'text': tval,
'scale': self._tip_title_scale,
'maxwidth': 122,
'h_align': 'right',
'vr_depth': -20,
'v_align': 'center',
'v_attach': 'bottom',
},
)
self._message_duration = 10000
self._message_spacing = 3000
self._change_timer = ba.Timer(
0.001 * (self._message_duration + self._message_spacing),
ba.WeakCall(self.change_phrase),
repeat=True,
)
self._combine = ba.newnode(
'combine',
owner=self.node,
attrs={'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4},
)
self._combine.connectattr('output', self.node, 'color')
self._combine.connectattr('output', self.title_node, 'color')
self.change_phrase()
def change_phrase(self) -> None:
"""Switch the visible tip phrase."""
from ba.internal import get_remote_app_name, get_next_tip
next_tip = ba.Lstr(
translate=('tips', get_next_tip()),
subs=[('${REMOTE_APP_NAME}', get_remote_app_name())],
)
spc = self._message_spacing
assert self.node
self.node.position = (-200, self._offs_y)
self.title_node.position = (-220, self._offs_y + 3)
keys = {
spc: 0,
spc + 1000: 1.0,
spc + self._message_duration - 1000: 1.0,
spc + self._message_duration: 0.0,
}
ba.animate(
self._combine,
'input3',
{k: v * 0.5 for k, v in list(keys.items())},
timeformat=ba.TimeFormat.MILLISECONDS,
)
self.node.text = next_tip
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
self.title_node.delete()
return None
return super().handlemessage(msg)

View file

@ -0,0 +1,209 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defined Actor(s)."""
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence
class ZoomText(ba.Actor):
"""Big Zooming Text.
Category: Gameplay Classes
Used for things such as the 'BOB WINS' victory messages.
"""
def __init__(
self,
text: str | ba.Lstr,
position: tuple[float, float] = (0.0, 0.0),
shiftposition: tuple[float, float] | None = None,
shiftdelay: float | None = None,
lifespan: float | None = None,
flash: bool = True,
trail: bool = True,
h_align: str = 'center',
color: Sequence[float] = (0.9, 0.4, 0.0),
jitter: float = 0.0,
trailcolor: Sequence[float] = (1.0, 0.35, 0.1, 0.0),
scale: float = 1.0,
project_scale: float = 1.0,
tilt_translate: float = 0.0,
maxwidth: float | None = None,
):
# pylint: disable=too-many-locals
super().__init__()
self._dying = False
positionadjusted = (position[0], position[1] - 100)
if shiftdelay is None:
shiftdelay = 2.500
if shiftdelay < 0.0:
ba.print_error('got shiftdelay < 0')
shiftdelay = 0.0
self._project_scale = project_scale
self.node = ba.newnode(
'text',
delegate=self,
attrs={
'position': positionadjusted,
'big': True,
'text': text,
'trail': trail,
'vr_depth': 0,
'shadow': 0.0 if trail else 0.3,
'scale': scale,
'maxwidth': maxwidth if maxwidth is not None else 0.0,
'tilt_translate': tilt_translate,
'h_align': h_align,
'v_align': 'center',
},
)
# we never jitter in vr mode..
if ba.app.vr_mode:
jitter = 0.0
# if they want jitter, animate its position slightly...
if jitter > 0.0:
self._jitter(positionadjusted, jitter * scale)
# if they want shifting, move to the shift position and
# then resume jittering
if shiftposition is not None:
positionadjusted2 = (shiftposition[0], shiftposition[1] - 100)
ba.timer(
shiftdelay,
ba.WeakCall(self._shift, positionadjusted, positionadjusted2),
)
if jitter > 0.0:
ba.timer(
shiftdelay + 0.25,
ba.WeakCall(
self._jitter, positionadjusted2, jitter * scale
),
)
color_combine = ba.newnode(
'combine',
owner=self.node,
attrs={'input2': color[2], 'input3': 1.0, 'size': 4},
)
if trail:
trailcolor_n = ba.newnode(
'combine',
owner=self.node,
attrs={
'size': 3,
'input0': trailcolor[0],
'input1': trailcolor[1],
'input2': trailcolor[2],
},
)
trailcolor_n.connectattr('output', self.node, 'trailcolor')
basemult = 0.85
ba.animate(
self.node,
'trail_project_scale',
{
0: 0 * project_scale,
basemult * 0.201: 0.6 * project_scale,
basemult * 0.347: 0.8 * project_scale,
basemult * 0.478: 0.9 * project_scale,
basemult * 0.595: 0.93 * project_scale,
basemult * 0.748: 0.95 * project_scale,
basemult * 0.941: 0.95 * project_scale,
},
)
if flash:
mult = 2.0
tm1 = 0.15
tm2 = 0.3
ba.animate(
color_combine,
'input0',
{0: color[0] * mult, tm1: color[0], tm2: color[0] * mult},
loop=True,
)
ba.animate(
color_combine,
'input1',
{0: color[1] * mult, tm1: color[1], tm2: color[1] * mult},
loop=True,
)
ba.animate(
color_combine,
'input2',
{0: color[2] * mult, tm1: color[2], tm2: color[2] * mult},
loop=True,
)
else:
color_combine.input0 = color[0]
color_combine.input1 = color[1]
color_combine.connectattr('output', self.node, 'color')
ba.animate(
self.node,
'project_scale',
{0: 0, 0.27: 1.05 * project_scale, 0.3: 1 * project_scale},
)
# if they give us a lifespan, kill ourself down the line
if lifespan is not None:
ba.timer(lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage()))
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if not self._dying and self.node:
self._dying = True
if msg.immediate:
self.node.delete()
else:
ba.animate(
self.node,
'project_scale',
{
0.0: 1 * self._project_scale,
0.6: 1.2 * self._project_scale,
},
)
ba.animate(self.node, 'opacity', {0.0: 1, 0.3: 0})
ba.animate(self.node, 'trail_opacity', {0.0: 1, 0.6: 0})
ba.timer(0.7, self.node.delete)
return None
return super().handlemessage(msg)
def _jitter(
self, position: tuple[float, float], jitter_amount: float
) -> None:
if not self.node:
return
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2})
for index, attr in enumerate(['input0', 'input1']):
keys = {}
timeval = 0.0
# gen some random keys for that stop-motion-y look
for _i in range(10):
keys[timeval] = (
position[index]
+ (random.random() - 0.5) * jitter_amount * 1.6
)
timeval += random.random() * 0.1
ba.animate(cmb, attr, keys, loop=True)
cmb.connectattr('output', self.node, 'position')
def _shift(
self, position1: tuple[float, float], position2: tuple[float, float]
) -> None:
if not self.node:
return
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2})
ba.animate(cmb, 'input0', {0.0: position1[0], 0.25: position2[0]})
ba.animate(cmb, 'input1', {0.0: position1[1], 0.25: position2[1]})
cmb.connectattr('output', self.node, 'position')