class BombFactory: """Wraps up media and other resources used by ba.Bombs. category: Gameplay Classes A single instance of this is shared between all bombs and can be retrieved via bastd.actor.bomb.get_factory(). Attributes: bomb_model The ba.Model of a standard or ice bomb. sticky_bomb_model The ba.Model of a sticky-bomb. impact_bomb_model The ba.Model of an impact-bomb. land_mine_model The ba.Model of a land-mine. tnt_model The ba.Model of a tnt box. regular_tex The ba.Texture for regular bombs. ice_tex The ba.Texture for ice bombs. sticky_tex The ba.Texture for sticky bombs. impact_tex The ba.Texture for impact bombs. impact_lit_tex The ba.Texture for impact bombs with lights lit. land_mine_tex The ba.Texture for land-mines. land_mine_lit_tex The ba.Texture for land-mines with the light lit. tnt_tex The ba.Texture for tnt boxes. hiss_sound The ba.Sound for the hiss sound an ice bomb makes. debris_fall_sound The ba.Sound for random falling debris after an explosion. wood_debris_fall_sound A ba.Sound for random wood debris falling after an explosion. explode_sounds A tuple of ba.Sounds for explosions. freeze_sound A ba.Sound of an ice bomb freezing something. fuse_sound A ba.Sound of a burning fuse. activate_sound A ba.Sound for an activating impact bomb. warn_sound A ba.Sound for an impact bomb about to explode due to time-out. bomb_material A ba.Material applied to all bombs. normal_sound_material A ba.Material that generates standard bomb noises on impacts, etc. sticky_material A ba.Material that makes 'splat' sounds and makes collisions softer. land_mine_no_explode_material A ba.Material that keeps land-mines from blowing up. Applied to land-mines when they are created to allow land-mines to touch without exploding. land_mine_blast_material A ba.Material applied to activated land-mines that causes them to explode on impact. impact_blast_material A ba.Material applied to activated impact-bombs that causes them to explode on impact. blast_material A ba.Material applied to bomb blast geometry which triggers impact events with what it touches. dink_sounds A tuple of ba.Sounds for when bombs hit the ground. sticky_impact_sound The ba.Sound for a squish made by a sticky bomb hitting something. roll_sound ba.Sound for a rolling bomb. """ _STORENAME = ba.storagename() @classmethod def get(cls) -> BombFactory: """Get/create a shared bastd.actor.bomb.BombFactory object.""" activity = ba.getactivity() factory = activity.customdata.get(cls._STORENAME) if factory is None: factory = BombFactory() activity.customdata[cls._STORENAME] = factory assert isinstance(factory, BombFactory) return factory def random_explode_sound(self) -> ba.Sound: """Return a random explosion ba.Sound from the factory.""" return self.explode_sounds[random.randrange(len(self.explode_sounds))] def __init__(self) -> None: """Instantiate a BombFactory. You shouldn't need to do this; call bastd.actor.bomb.get_factory() to get a shared instance. """ shared = SharedObjects.get() self.bomb_model = ba.getmodel('bomb') self.sticky_bomb_model = ba.getmodel('bombSticky') self.impact_bomb_model = ba.getmodel('impactBomb') self.land_mine_model = ba.getmodel('landMine') self.tnt_model = ba.getmodel('tnt') self.regular_tex = ba.gettexture('bombColor') self.ice_tex = ba.gettexture('bombColorIce') self.sticky_tex = ba.gettexture('bombStickyColor') self.impact_tex = ba.gettexture('impactBombColor') self.impact_lit_tex = ba.gettexture('impactBombColorLit') self.land_mine_tex = ba.gettexture('landMine') self.land_mine_lit_tex = ba.gettexture('landMineLit') self.tnt_tex = ba.gettexture('tnt') self.hiss_sound = ba.getsound('hiss') self.debris_fall_sound = ba.getsound('debrisFall') self.wood_debris_fall_sound = ba.getsound('woodDebrisFall') self.explode_sounds = (ba.getsound('explosion01'), ba.getsound('explosion02'), ba.getsound('explosion03'), ba.getsound('explosion04'), ba.getsound('explosion05')) self.freeze_sound = ba.getsound('freeze') self.fuse_sound = ba.getsound('fuse01') self.activate_sound = ba.getsound('activateBeep') self.warn_sound = ba.getsound('warnBeep') # Set up our material so new bombs don't collide with objects # that they are initially overlapping. self.bomb_material = ba.Material() self.normal_sound_material = ba.Material() self.sticky_material = ba.Material() self.bomb_material.add_actions( conditions=( ( ('we_are_younger_than', 100), 'or', ('they_are_younger_than', 100), ), 'and', ('they_have_material', shared.object_material), ), actions=('modify_node_collision', 'collide', False), ) # We want pickup materials to always hit us even if we're currently # not colliding with their node. (generally due to the above rule) self.bomb_material.add_actions( conditions=('they_have_material', shared.pickup_material), actions=('modify_part_collision', 'use_node_collide', False), ) self.bomb_material.add_actions(actions=('modify_part_collision', 'friction', 0.3)) self.land_mine_no_explode_material = ba.Material() self.land_mine_blast_material = ba.Material() self.land_mine_blast_material.add_actions( conditions=( ('we_are_older_than', 200), 'and', ('they_are_older_than', 200), 'and', ('eval_colliding', ), 'and', ( ('they_dont_have_material', self.land_mine_no_explode_material), 'and', ( ('they_have_material', shared.object_material), 'or', ('they_have_material', shared.player_material), ), ), ), actions=('message', 'our_node', 'at_connect', ImpactMessage()), ) self.impact_blast_material = ba.Material() self.impact_blast_material.add_actions( conditions=( ('we_are_older_than', 200), 'and', ('they_are_older_than', 200), 'and', ('eval_colliding', ), 'and', ( ('they_have_material', shared.footing_material), 'or', ('they_have_material', shared.object_material), ), ), actions=('message', 'our_node', 'at_connect', ImpactMessage()), ) self.blast_material = ba.Material() self.blast_material.add_actions( conditions=('they_have_material', shared.object_material), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('message', 'our_node', 'at_connect', ExplodeHitMessage()), ), ) self.dink_sounds = (ba.getsound('bombDrop01'), ba.getsound('bombDrop02')) self.sticky_impact_sound = ba.getsound('stickyImpact') self.roll_sound = ba.getsound('bombRoll01') # Collision sounds. self.normal_sound_material.add_actions( conditions=('they_have_material', shared.footing_material), actions=( ('impact_sound', self.dink_sounds, 2, 0.8), ('roll_sound', self.roll_sound, 3, 6), )) self.sticky_material.add_actions(actions=(('modify_part_collision', 'stiffness', 0.1), ('modify_part_collision', 'damping', 1.0))) self.sticky_material.add_actions( conditions=( ('they_have_material', shared.player_material), 'or', ('they_have_material', shared.footing_material), ), actions=('message', 'our_node', 'at_connect', SplatMessage()), )
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, 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, countdown: bool = False, flash: bool = True, show_value: bool = True) -> None: """Update the score-board display for the given ba.Team.""" if not team.id 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
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. Attributes: impact_sounds_medium A tuple of ba.Sounds for when a ba.Spaz hits something kinda hard. impact_sounds_hard A tuple of ba.Sounds for when a ba.Spaz hits something really hard. impact_sounds_harder A tuple of ba.Sounds for when a ba.Spaz hits something really really hard. single_player_death_sound The sound that plays for an 'important' spaz death such as in co-op games. punch_sound A standard punch ba.Sound. punch_sound_strong A tuple of stronger sounding punch ba.Sounds. punch_sound_stronger A really really strong sounding punch ba.Sound. swish_sound A punch swish ba.Sound. block_sound A ba.Sound for when an attack is blocked by invincibility. shatter_sound A ba.Sound for when a frozen ba.Spaz shatters. splatter_sound A ba.Sound for when a ba.Spaz blows up via curse. spaz_material A ba.Material applied to all of parts of a ba.Spaz. roller_material A ba.Material applied to the invisible roller ball body that a ba.Spaz uses for locomotion. punch_material A ba.Material applied to the 'fist' of a ba.Spaz. pickup_material A ba.Material applied to the 'grabber' body of a ba.Spaz. curse_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 = 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.get_account_misc_read_val('rsdr', 10.0) self.punch_cooldown = _ba.get_account_misc_read_val('rpc', 400) self.punch_cooldown_gloves = (_ba.get_account_misc_read_val( 'rpcg', 300)) self.punch_power_scale = _ba.get_account_misc_read_val('rpp', 1.2) self.punch_power_scale_gloves = (_ba.get_account_misc_read_val( 'rppg', 1.4)) self.max_shield_spillover_damage = (_ba.get_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
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: Optional[str] = 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, excludetypes: list[str] = 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 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: Optional[ba.NodeActor] = 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: Optional[ba.NodeActor] = 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: Optional[ba.NodeActor] = 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: Optional[ba.Timer] = 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
class SharedObjects: """Various common components for use in games. Category: Gameplay Classes Objects contained here are created on-demand as accessed and shared by everything in the current activity. This includes things such as standard materials. """ _STORENAME = ba.storagename() def __init__(self) -> None: activity = ba.getactivity() if self._STORENAME in activity.customdata: raise RuntimeError('Use SharedObjects.get() to fetch the' ' shared instance for this activity.') self._object_material: Optional[ba.Material] = None self._player_material: Optional[ba.Material] = None self._pickup_material: Optional[ba.Material] = None self._footing_material: Optional[ba.Material] = None self._attack_material: Optional[ba.Material] = None self._death_material: Optional[ba.Material] = None self._region_material: Optional[ba.Material] = None self._railing_material: Optional[ba.Material] = None @classmethod def get(cls) -> SharedObjects: """Fetch/create the instance of this class for the current activity.""" activity = ba.getactivity() shobs = activity.customdata.get(cls._STORENAME) if shobs is None: shobs = SharedObjects() activity.customdata[cls._STORENAME] = shobs assert isinstance(shobs, SharedObjects) return shobs @property def player_material(self) -> ba.Material: """a ba.Material to be applied to player parts. Generally, materials related to the process of scoring when reaching a goal, etc will look for the presence of this material on things that hit them. """ if self._player_material is None: self._player_material = ba.Material() return self._player_material @property def object_material(self) -> ba.Material: """A ba.Material that should be applied to any small, normal, physical objects such as bombs, boxes, players, etc. Other materials often check for the presence of this material as a prerequisite for performing certain actions (such as disabling collisions between initially-overlapping objects) """ if self._object_material is None: self._object_material = ba.Material() return self._object_material @property def pickup_material(self) -> ba.Material: """A ba.Material; collision shapes used for picking things up will have this material applied. To prevent an object from being picked up, you can add a material that disables collisions against things containing this material. """ if self._pickup_material is None: self._pickup_material = ba.Material() return self._pickup_material @property def footing_material(self) -> ba.Material: """Anything that can be 'walked on' should have this ba.Material applied; generally just terrain and whatnot. A character will snap upright whenever touching something with this material so it should not be applied to props, etc. """ if self._footing_material is None: self._footing_material = ba.Material() return self._footing_material @property def attack_material(self) -> ba.Material: """A ba.Material applied to explosion shapes, punch shapes, etc. An object not wanting to receive impulse/etc messages can disable collisions against this material. """ if self._attack_material is None: self._attack_material = ba.Material() return self._attack_material @property def death_material(self) -> ba.Material: """A ba.Material that sends a ba.DieMessage() to anything that touches it; handy for terrain below a cliff, etc. """ if self._death_material is None: mat = self._death_material = ba.Material() mat.add_actions( ('message', 'their_node', 'at_connect', ba.DieMessage())) return self._death_material @property def region_material(self) -> ba.Material: """A ba.Material used for non-physical collision shapes (regions); collisions can generally be allowed with this material even when initially overlapping since it is not physical. """ if self._region_material is None: self._region_material = ba.Material() return self._region_material @property def railing_material(self) -> ba.Material: """A ba.Material with a very low friction/stiffness/etc that can be applied to invisible 'railings' useful for gently keeping characters from falling off of cliffs. """ if self._railing_material is None: mat = self._railing_material = ba.Material() mat.add_actions(('modify_part_collision', 'collide', False)) mat.add_actions(('modify_part_collision', 'stiffness', 0.003)) mat.add_actions(('modify_part_collision', 'damping', 0.00001)) mat.add_actions( conditions=('they_have_material', self.player_material), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'friction', 0.0), ), ) return self._railing_material
class FlagFactory: """Wraps up media and other resources used by ba.Flags. category: Gameplay Classes A single instance of this is shared between all flags and can be retrieved via bastd.actor.flag.get_factory(). Attributes: flagmaterial The ba.Material applied to all ba.Flags. impact_sound The ba.Sound used when a ba.Flag hits the ground. skid_sound The ba.Sound used when a ba.Flag skids along the ground. no_hit_material A ba.Material that prevents contact with most objects; applied to 'non-touchable' flags. flag_texture The ba.Texture for flags. """ _STORENAME = ba.storagename() def __init__(self) -> None: """Instantiate a FlagFactory. You shouldn't need to do this; call bastd.actor.flag.get_factory() 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('flagColor') @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