# 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