Source code for bascenev1lib.actor.bomb

# Released under the MIT License. See LICENSE for details.
#
"""Various classes for bombs, mines, tnt, etc."""

# FIXME
# pylint: disable=too-many-lines

from __future__ import annotations

import random
from typing import TYPE_CHECKING, TypeVar, override

import bascenev1 as bs

from bascenev1lib.gameutils import SharedObjects

if TYPE_CHECKING:
    from typing import Any, Sequence, Callable

PlayerT = TypeVar('PlayerT', bound='bs.Player')


[docs] class BombFactory: """Wraps up media and other resources used by bs.Bombs. Category: **Gameplay Classes** A single instance of this is shared between all bombs and can be retrieved via bascenev1lib.actor.bomb.get_factory(). """ bomb_mesh: bs.Mesh """The bs.Mesh of a standard or ice bomb.""" sticky_bomb_mesh: bs.Mesh """The bs.Mesh of a sticky-bomb.""" impact_bomb_mesh: bs.Mesh """The bs.Mesh of an impact-bomb.""" land_mine_mesh: bs.Mesh """The bs.Mesh of a land-mine.""" tnt_mesh: bs.Mesh """The bs.Mesh of a tnt box.""" regular_tex: bs.Texture """The bs.Texture for regular bombs.""" ice_tex: bs.Texture """The bs.Texture for ice bombs.""" sticky_tex: bs.Texture """The bs.Texture for sticky bombs.""" impact_tex: bs.Texture """The bs.Texture for impact bombs.""" impact_lit_tex: bs.Texture """The bs.Texture for impact bombs with lights lit.""" land_mine_tex: bs.Texture """The bs.Texture for land-mines.""" land_mine_lit_tex: bs.Texture """The bs.Texture for land-mines with the light lit.""" tnt_tex: bs.Texture """The bs.Texture for tnt boxes.""" hiss_sound: bs.Sound """The bs.Sound for the hiss sound an ice bomb makes.""" debris_fall_sound: bs.Sound """The bs.Sound for random falling debris after an explosion.""" wood_debris_fall_sound: bs.Sound """A bs.Sound for random wood debris falling after an explosion.""" explode_sounds: Sequence[bs.Sound] """A tuple of bs.Sound-s for explosions.""" freeze_sound: bs.Sound """A bs.Sound of an ice bomb freezing something.""" fuse_sound: bs.Sound """A bs.Sound of a burning fuse.""" activate_sound: bs.Sound """A bs.Sound for an activating impact bomb.""" warn_sound: bs.Sound """A bs.Sound for an impact bomb about to explode due to time-out.""" bomb_material: bs.Material """A bs.Material applied to all bombs.""" normal_sound_material: bs.Material """A bs.Material that generates standard bomb noises on impacts, etc.""" sticky_material: bs.Material """A bs.Material that makes 'splat' sounds and makes collisions softer.""" land_mine_no_explode_material: bs.Material """A bs.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: bs.Material """A bs.Material applied to activated land-mines that causes them to explode on impact.""" impact_blast_material: bs.Material """A bs.Material applied to activated impact-bombs that causes them to explode on impact.""" blast_material: bs.Material """A bs.Material applied to bomb blast geometry which triggers impact events with what it touches.""" dink_sounds: Sequence[bs.Sound] """A tuple of bs.Sound-s for when bombs hit the ground.""" sticky_impact_sound: bs.Sound """The bs.Sound for a squish made by a sticky bomb hitting something.""" roll_sound: bs.Sound """bs.Sound for a rolling bomb.""" _STORENAME = bs.storagename()
[docs] @classmethod def get(cls) -> BombFactory: """Get/create a shared bascenev1lib.actor.bomb.BombFactory object.""" activity = bs.getactivity() factory = activity.customdata.get(cls._STORENAME) if factory is None: factory = BombFactory() activity.customdata[cls._STORENAME] = factory assert isinstance(factory, BombFactory) return factory
[docs] def random_explode_sound(self) -> bs.Sound: """Return a random explosion bs.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 bascenev1lib.actor.bomb.get_factory() to get a shared instance. """ shared = SharedObjects.get() self.bomb_mesh = bs.getmesh('bomb') self.sticky_bomb_mesh = bs.getmesh('bombSticky') self.impact_bomb_mesh = bs.getmesh('impactBomb') self.land_mine_mesh = bs.getmesh('landMine') self.tnt_mesh = bs.getmesh('tnt') self.regular_tex = bs.gettexture('bombColor') self.ice_tex = bs.gettexture('bombColorIce') self.sticky_tex = bs.gettexture('bombStickyColor') self.impact_tex = bs.gettexture('impactBombColor') self.impact_lit_tex = bs.gettexture('impactBombColorLit') self.land_mine_tex = bs.gettexture('landMine') self.land_mine_lit_tex = bs.gettexture('landMineLit') self.tnt_tex = bs.gettexture('tnt') self.hiss_sound = bs.getsound('hiss') self.debris_fall_sound = bs.getsound('debrisFall') self.wood_debris_fall_sound = bs.getsound('woodDebrisFall') self.explode_sounds = ( bs.getsound('explosion01'), bs.getsound('explosion02'), bs.getsound('explosion03'), bs.getsound('explosion04'), bs.getsound('explosion05'), ) self.freeze_sound = bs.getsound('freeze') self.fuse_sound = bs.getsound('fuse01') self.activate_sound = bs.getsound('activateBeep') self.warn_sound = bs.getsound('warnBeep') # Set up our material so new bombs don't collide with objects # that they are initially overlapping. self.bomb_material = bs.Material() self.normal_sound_material = bs.Material() self.sticky_material = bs.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 = bs.Material() self.land_mine_blast_material = bs.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 = bs.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 = bs.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 = ( bs.getsound('bombDrop01'), bs.getsound('bombDrop02'), ) self.sticky_impact_sound = bs.getsound('stickyImpact') self.roll_sound = bs.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()), )
[docs] class SplatMessage: """Tells an object to make a splat noise."""
[docs] class ExplodeMessage: """Tells an object to explode."""
[docs] class ImpactMessage: """Tell an object it touched something."""
[docs] class ArmMessage: """Tell an object to become armed."""
[docs] class WarnMessage: """Tell an object to issue a warning sound."""
[docs] class ExplodeHitMessage: """Tell an object it was hit by an explosion."""
[docs] class Blast(bs.Actor): """An explosion, as generated by a bomb or some other object. category: Gameplay Classes """ def __init__( self, *, position: Sequence[float] = (0.0, 1.0, 0.0), velocity: Sequence[float] = (0.0, 0.0, 0.0), blast_radius: float = 2.0, blast_type: str = 'normal', source_player: bs.Player | None = None, hit_type: str = 'explosion', hit_subtype: str = 'normal', ): """Instantiate with given values.""" # bah; get off my lawn! # pylint: disable=too-many-locals # pylint: disable=too-many-statements super().__init__() shared = SharedObjects.get() factory = BombFactory.get() self.blast_type = blast_type self._source_player = source_player self.hit_type = hit_type self.hit_subtype = hit_subtype self.radius = blast_radius # Set our position a bit lower so we throw more things upward. rmats = (factory.blast_material, shared.attack_material) self.node = bs.newnode( 'region', delegate=self, attrs={ 'position': (position[0], position[1] - 0.1, position[2]), 'scale': (self.radius, self.radius, self.radius), 'type': 'sphere', 'materials': rmats, }, ) bs.timer(0.05, self.node.delete) # Throw in an explosion and flash. evel = (velocity[0], max(-1.0, velocity[1]), velocity[2]) explosion = bs.newnode( 'explosion', attrs={ 'position': position, 'velocity': evel, 'radius': self.radius, 'big': (self.blast_type == 'tnt'), }, ) if self.blast_type == 'ice': explosion.color = (0, 0.05, 0.4) bs.timer(1.0, explosion.delete) if self.blast_type != 'ice': bs.emitfx( position=position, velocity=velocity, count=int(1.0 + random.random() * 4), emit_type='tendrils', tendril_type='thin_smoke', ) bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 4), emit_type='tendrils', tendril_type='ice' if self.blast_type == 'ice' else 'smoke', ) bs.emitfx( position=position, emit_type='distortion', spread=1.0 if self.blast_type == 'tnt' else 2.0, ) # And emit some shrapnel. if self.blast_type == 'ice': def emit() -> None: bs.emitfx( position=position, velocity=velocity, count=30, spread=2.0, scale=0.4, chunk_type='ice', emit_type='stickers', ) # It looks better if we delay a bit. bs.timer(0.05, emit) elif self.blast_type == 'sticky': def emit() -> None: bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), spread=0.7, chunk_type='slime', ) bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), scale=0.5, spread=0.7, chunk_type='slime', ) bs.emitfx( position=position, velocity=velocity, count=15, scale=0.6, chunk_type='slime', emit_type='stickers', ) bs.emitfx( position=position, velocity=velocity, count=20, scale=0.7, chunk_type='spark', emit_type='stickers', ) bs.emitfx( position=position, velocity=velocity, count=int(6.0 + random.random() * 12), scale=0.8, spread=1.5, chunk_type='spark', ) # It looks better if we delay a bit. bs.timer(0.05, emit) elif self.blast_type == 'impact': def emit() -> None: bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), scale=0.8, chunk_type='metal', ) bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), scale=0.4, chunk_type='metal', ) bs.emitfx( position=position, velocity=velocity, count=20, scale=0.7, chunk_type='spark', emit_type='stickers', ) bs.emitfx( position=position, velocity=velocity, count=int(8.0 + random.random() * 15), scale=0.8, spread=1.5, chunk_type='spark', ) # It looks better if we delay a bit. bs.timer(0.05, emit) else: # Regular or land mine bomb shrapnel. def emit() -> None: if self.blast_type != 'tnt': bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), chunk_type='rock', ) bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), scale=0.5, chunk_type='rock', ) bs.emitfx( position=position, velocity=velocity, count=30, scale=1.0 if self.blast_type == 'tnt' else 0.7, chunk_type='spark', emit_type='stickers', ) bs.emitfx( position=position, velocity=velocity, count=int(18.0 + random.random() * 20), scale=1.0 if self.blast_type == 'tnt' else 0.8, spread=1.5, chunk_type='spark', ) # TNT throws splintery chunks. if self.blast_type == 'tnt': def emit_splinters() -> None: bs.emitfx( position=position, velocity=velocity, count=int(20.0 + random.random() * 25), scale=0.8, spread=1.0, chunk_type='splinter', ) bs.timer(0.01, emit_splinters) # Every now and then do a sparky one. if self.blast_type == 'tnt' or random.random() < 0.1: def emit_extra_sparks() -> None: bs.emitfx( position=position, velocity=velocity, count=int(10.0 + random.random() * 20), scale=0.8, spread=1.5, chunk_type='spark', ) bs.timer(0.02, emit_extra_sparks) # It looks better if we delay a bit. bs.timer(0.05, emit) lcolor = (0.6, 0.6, 1.0) if self.blast_type == 'ice' else (1, 0.3, 0.1) light = bs.newnode( 'light', attrs={ 'position': position, 'volume_intensity_scale': 10.0, 'color': lcolor, }, ) scl = random.uniform(0.6, 0.9) scorch_radius = light_radius = self.radius if self.blast_type == 'tnt': light_radius *= 1.4 scorch_radius *= 1.15 scl *= 3.0 iscale = 1.6 bs.animate( light, 'intensity', { 0: 2.0 * iscale, scl * 0.02: 0.1 * iscale, scl * 0.025: 0.2 * iscale, scl * 0.05: 17.0 * iscale, scl * 0.06: 5.0 * iscale, scl * 0.08: 4.0 * iscale, scl * 0.2: 0.6 * iscale, scl * 2.0: 0.00 * iscale, scl * 3.0: 0.0, }, ) bs.animate( light, 'radius', { 0: light_radius * 0.2, scl * 0.05: light_radius * 0.55, scl * 0.1: light_radius * 0.3, scl * 0.3: light_radius * 0.15, scl * 1.0: light_radius * 0.05, }, ) bs.timer(scl * 3.0, light.delete) # Make a scorch that fades over time. scorch = bs.newnode( 'scorch', attrs={ 'position': position, 'size': scorch_radius * 0.5, 'big': (self.blast_type == 'tnt'), }, ) if self.blast_type == 'ice': scorch.color = (1, 1, 1.5) bs.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) bs.timer(13.0, scorch.delete) if self.blast_type == 'ice': factory.hiss_sound.play(position=light.position) lpos = light.position factory.random_explode_sound().play(position=lpos) factory.debris_fall_sound.play(position=lpos) bs.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0) # TNT is more epic. if self.blast_type == 'tnt': factory.random_explode_sound().play(position=lpos) def _extra_boom() -> None: factory.random_explode_sound().play(position=lpos) bs.timer(0.25, _extra_boom) def _extra_debris_sound() -> None: factory.debris_fall_sound.play(position=lpos) factory.wood_debris_fall_sound.play(position=lpos) bs.timer(0.4, _extra_debris_sound)
[docs] @override def handlemessage(self, msg: Any) -> Any: assert not self.expired if isinstance(msg, bs.DieMessage): if self.node: self.node.delete() elif isinstance(msg, ExplodeHitMessage): node = bs.getcollision().opposingnode assert self.node nodepos = self.node.position mag = 2000.0 if self.blast_type == 'ice': mag *= 0.5 elif self.blast_type == 'land_mine': mag *= 2.5 elif self.blast_type == 'tnt': mag *= 2.0 node.handlemessage( bs.HitMessage( pos=nodepos, velocity=(0, 0, 0), magnitude=mag, hit_type=self.hit_type, hit_subtype=self.hit_subtype, radius=self.radius, source_player=bs.existing(self._source_player), ) ) if self.blast_type == 'ice': BombFactory.get().freeze_sound.play(10, position=nodepos) node.handlemessage(bs.FreezeMessage()) else: return super().handlemessage(msg) return None
[docs] class Bomb(bs.Actor): """A standard bomb and its variants such as land-mines and tnt-boxes. category: Gameplay Classes """ # Ew; should try to clean this up later. # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements def __init__( self, *, position: Sequence[float] = (0.0, 1.0, 0.0), velocity: Sequence[float] = (0.0, 0.0, 0.0), bomb_type: str = 'normal', blast_radius: float = 2.0, bomb_scale: float = 1.0, source_player: bs.Player | None = None, owner: bs.Node | None = None, ): """Create a new Bomb. bomb_type can be 'ice','impact','land_mine','normal','sticky', or 'tnt'. Note that for impact or land_mine bombs you have to call arm() before they will go off. """ super().__init__() shared = SharedObjects.get() factory = BombFactory.get() if bomb_type not in ( 'ice', 'impact', 'land_mine', 'normal', 'sticky', 'tnt', ): raise ValueError('invalid bomb type: ' + bomb_type) self.bomb_type = bomb_type self._exploded = False self.scale = bomb_scale self.texture_sequence: bs.Node | None = None if self.bomb_type == 'sticky': self._last_sticky_sound_time = 0.0 self.blast_radius = blast_radius if self.bomb_type == 'ice': self.blast_radius *= 1.2 elif self.bomb_type == 'impact': self.blast_radius *= 0.7 elif self.bomb_type == 'land_mine': self.blast_radius *= 0.7 elif self.bomb_type == 'tnt': self.blast_radius *= 1.45 self._explode_callbacks: list[Callable[[Bomb, Blast], Any]] = [] # The player this came from. self._source_player = source_player # By default our hit type/subtype is our own, but we pick up types of # whoever sets us off so we know what caused a chain reaction. # UPDATE (July 2020): not inheriting hit-types anymore; this causes # weird effects such as land-mines inheriting 'punch' hit types and # then not being able to destroy certain things they normally could, # etc. Inheriting owner/source-node from things that set us off # should be all we need I think... self.hit_type = 'explosion' self.hit_subtype = self.bomb_type # The node this came from. # FIXME: can we unify this and source_player? self.owner = owner # Adding footing-materials to things can screw up jumping and flying # since players carrying those things and thus touching footing # objects will think they're on solid ground.. perhaps we don't # wanna add this even in the tnt case? materials: tuple[bs.Material, ...] if self.bomb_type == 'tnt': materials = ( factory.bomb_material, shared.footing_material, shared.object_material, ) else: materials = (factory.bomb_material, shared.object_material) if self.bomb_type == 'impact': materials = materials + (factory.impact_blast_material,) elif self.bomb_type == 'land_mine': materials = materials + (factory.land_mine_no_explode_material,) if self.bomb_type == 'sticky': materials = materials + (factory.sticky_material,) else: materials = materials + (factory.normal_sound_material,) if self.bomb_type == 'land_mine': fuse_time = None self.node = bs.newnode( 'prop', delegate=self, attrs={ 'position': position, 'velocity': velocity, 'mesh': factory.land_mine_mesh, 'light_mesh': factory.land_mine_mesh, 'body': 'landMine', 'body_scale': self.scale, 'shadow_size': 0.44, 'color_texture': factory.land_mine_tex, 'reflection': 'powerup', 'reflection_scale': [1.0], 'materials': materials, }, ) elif self.bomb_type == 'tnt': fuse_time = None self.node = bs.newnode( 'prop', delegate=self, attrs={ 'position': position, 'velocity': velocity, 'mesh': factory.tnt_mesh, 'light_mesh': factory.tnt_mesh, 'body': 'crate', 'body_scale': self.scale, 'shadow_size': 0.5, 'color_texture': factory.tnt_tex, 'reflection': 'soft', 'reflection_scale': [0.23], 'materials': materials, }, ) elif self.bomb_type == 'impact': fuse_time = 20.0 self.node = bs.newnode( 'prop', delegate=self, attrs={ 'position': position, 'velocity': velocity, 'body': 'sphere', 'body_scale': self.scale, 'mesh': factory.impact_bomb_mesh, 'shadow_size': 0.3, 'color_texture': factory.impact_tex, 'reflection': 'powerup', 'reflection_scale': [1.5], 'materials': materials, }, ) self.arm_timer = bs.Timer( 0.2, bs.WeakCall(self.handlemessage, ArmMessage()) ) self.warn_timer = bs.Timer( fuse_time - 1.7, bs.WeakCall(self.handlemessage, WarnMessage()) ) else: fuse_time = 3.0 if self.bomb_type == 'sticky': sticky = True mesh = factory.sticky_bomb_mesh rtype = 'sharper' rscale = 1.8 else: sticky = False mesh = factory.bomb_mesh rtype = 'sharper' rscale = 1.8 if self.bomb_type == 'ice': tex = factory.ice_tex elif self.bomb_type == 'sticky': tex = factory.sticky_tex else: tex = factory.regular_tex self.node = bs.newnode( 'bomb', delegate=self, attrs={ 'position': position, 'velocity': velocity, 'mesh': mesh, 'body_scale': self.scale, 'shadow_size': 0.3, 'color_texture': tex, 'sticky': sticky, 'owner': owner, 'reflection': rtype, 'reflection_scale': [rscale], 'materials': materials, }, ) sound = bs.newnode( 'sound', owner=self.node, attrs={'sound': factory.fuse_sound, 'volume': 0.25}, ) self.node.connectattr('position', sound, 'position') bs.animate(self.node, 'fuse_length', {0.0: 1.0, fuse_time: 0.0}) # Light the fuse!!! if self.bomb_type not in ('land_mine', 'tnt'): assert fuse_time is not None bs.timer( fuse_time, bs.WeakCall(self.handlemessage, ExplodeMessage()) ) bs.animate( self.node, 'mesh_scale', {0: 0, 0.2: 1.3 * self.scale, 0.26: self.scale}, )
[docs] def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None: """Return the source-player if one exists and is the provided type.""" player: Any = self._source_player return ( player if isinstance(player, playertype) and player.exists() else None )
[docs] @override def on_expire(self) -> None: super().on_expire() # Release callbacks/refs so we don't wind up with dependency loops. self._explode_callbacks = []
def _handle_die(self) -> None: if self.node: self.node.delete() def _handle_oob(self) -> None: self.handlemessage(bs.DieMessage()) def _handle_impact(self) -> None: node = bs.getcollision().opposingnode # If we're an impact bomb and we came from this node, don't explode. # (otherwise we blow up on our own head when jumping). # Alternately if we're hitting another impact-bomb from the same # source, don't explode. (can cause accidental explosions if rapidly # throwing/etc.) node_delegate = node.getdelegate(object) if node: if self.bomb_type == 'impact' and ( node is self.owner or ( isinstance(node_delegate, Bomb) and node_delegate.bomb_type == 'impact' and node_delegate.owner is self.owner ) ): return self.handlemessage(ExplodeMessage()) def _handle_dropped(self) -> None: if self.bomb_type == 'land_mine': self.arm_timer = bs.Timer( 1.25, bs.WeakCall(self.handlemessage, ArmMessage()) ) # Once we've thrown a sticky bomb we can stick to it. elif self.bomb_type == 'sticky': def _setsticky(node: bs.Node) -> None: if node: node.stick_to_owner = True bs.timer(0.25, lambda: _setsticky(self.node)) def _handle_splat(self) -> None: node = bs.getcollision().opposingnode if ( node is not self.owner and bs.time() - self._last_sticky_sound_time > 1.0 ): self._last_sticky_sound_time = bs.time() assert self.node BombFactory.get().sticky_impact_sound.play( 2.0, position=self.node.position, )
[docs] def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None: """Add a call to be run when the bomb has exploded. The bomb and the new blast object are passed as arguments. """ self._explode_callbacks.append(call)
[docs] def explode(self) -> None: """Blows up the bomb if it has not yet done so.""" if self._exploded: return self._exploded = True if self.node: blast = Blast( position=self.node.position, velocity=self.node.velocity, blast_radius=self.blast_radius, blast_type=self.bomb_type, source_player=bs.existing(self._source_player), hit_type=self.hit_type, hit_subtype=self.hit_subtype, ).autoretain() for callback in self._explode_callbacks: callback(self, blast) # We blew up so we need to go away. # NOTE TO SELF: do we actually need this delay? bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage()))
def _handle_warn(self) -> None: if self.texture_sequence and self.node: self.texture_sequence.rate = 30 BombFactory.get().warn_sound.play(0.5, position=self.node.position) def _add_material(self, material: bs.Material) -> None: if not self.node: return materials = self.node.materials if material not in materials: assert isinstance(materials, tuple) self.node.materials = materials + (material,)
[docs] def arm(self) -> None: """Arm the bomb (for land-mines and impact-bombs). These types of bombs will not explode until they have been armed. """ if not self.node: return factory = BombFactory.get() intex: Sequence[bs.Texture] if self.bomb_type == 'land_mine': intex = (factory.land_mine_lit_tex, factory.land_mine_tex) self.texture_sequence = bs.newnode( 'texture_sequence', owner=self.node, attrs={'rate': 30, 'input_textures': intex}, ) bs.timer(0.5, self.texture_sequence.delete) # We now make it explodable. bs.timer( 0.25, bs.WeakCall( self._add_material, factory.land_mine_blast_material ), ) elif self.bomb_type == 'impact': intex = ( factory.impact_lit_tex, factory.impact_tex, factory.impact_tex, ) self.texture_sequence = bs.newnode( 'texture_sequence', owner=self.node, attrs={'rate': 100, 'input_textures': intex}, ) bs.timer( 0.25, bs.WeakCall( self._add_material, factory.land_mine_blast_material ), ) else: raise RuntimeError( 'arm() should only be called on land-mines or impact bombs' ) self.texture_sequence.connectattr( 'output_texture', self.node, 'color_texture' ) factory.activate_sound.play(0.5, position=self.node.position)
def _handle_hit(self, msg: bs.HitMessage) -> None: ispunched = msg.srcnode and msg.srcnode.getnodetype() == 'spaz' # Normal bombs are triggered by non-punch impacts; # impact-bombs by all impacts. if not self._exploded and ( not ispunched or self.bomb_type in ['impact', 'land_mine'] ): # Also lets change the owner of the bomb to whoever is setting # us off. (this way points for big chain reactions go to the # person causing them). source_player = msg.get_source_player(bs.Player) if source_player is not None: self._source_player = source_player # Also inherit the hit type (if a landmine sets off by a bomb, # the credit should go to the mine) # the exception is TNT. TNT always gets credit. # UPDATE (July 2020): not doing this anymore. Causes too much # weird logic such as bombs acting like punches. Holler if # anything is noticeably broken due to this. # if self.bomb_type != 'tnt': # self.hit_type = msg.hit_type # self.hit_subtype = msg.hit_subtype bs.timer( 0.1 + random.random() * 0.1, bs.WeakCall(self.handlemessage, ExplodeMessage()), ) assert self.node 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.velocity[0], msg.velocity[1], msg.velocity[2], ) if msg.srcnode: pass
[docs] @override def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ExplodeMessage): self.explode() elif isinstance(msg, ImpactMessage): self._handle_impact() elif isinstance(msg, bs.PickedUpMessage): # Change our source to whoever just picked us up *only* if it # is None. This way we can get points for killing bots with their # own bombs. Hmm would there be a downside to this? if self._source_player is None: self._source_player = msg.node.source_player elif isinstance(msg, SplatMessage): self._handle_splat() elif isinstance(msg, bs.DroppedMessage): self._handle_dropped() elif isinstance(msg, bs.HitMessage): self._handle_hit(msg) elif isinstance(msg, bs.DieMessage): self._handle_die() elif isinstance(msg, bs.OutOfBoundsMessage): self._handle_oob() elif isinstance(msg, ArmMessage): self.arm() elif isinstance(msg, WarnMessage): self._handle_warn() else: super().handlemessage(msg)
[docs] class TNTSpawner: """Regenerates TNT at a given point in space every now and then. category: Gameplay Classes """ def __init__(self, position: Sequence[float], respawn_time: float = 20.0): """Instantiate with given position and respawn_time (in seconds).""" self._position = position self._tnt: Bomb | None = None self._respawn_time = random.uniform(0.8, 1.2) * respawn_time self._wait_time = 0.0 self._update() # Go with slightly more than 1 second to avoid timer stacking. self._update_timer = bs.Timer( 1.1, bs.WeakCall(self._update), repeat=True ) def _update(self) -> None: tnt_alive = self._tnt is not None and self._tnt.node if not tnt_alive: # Respawn if its been long enough.. otherwise just increment our # how-long-since-we-died value. if self._tnt is None or self._wait_time >= self._respawn_time: self._tnt = Bomb(position=self._position, bomb_type='tnt') self._wait_time = 0.0 else: self._wait_time += 1.1