bastd.actor.bomb

Various classes for bombs, mines, tnt, etc.

   1# Released under the MIT License. See LICENSE for details.
   2#
   3"""Various classes for bombs, mines, tnt, etc."""
   4
   5# FIXME
   6# pylint: disable=too-many-lines
   7
   8from __future__ import annotations
   9
  10import random
  11from typing import TYPE_CHECKING, TypeVar
  12
  13import ba
  14from bastd.gameutils import SharedObjects
  15
  16if TYPE_CHECKING:
  17    from typing import Any, Sequence, Callable
  18
  19# pylint: disable=invalid-name
  20PlayerType = TypeVar('PlayerType', bound='ba.Player')
  21# pylint: enable=invalid-name
  22
  23
  24class BombFactory:
  25    """Wraps up media and other resources used by ba.Bombs.
  26
  27    Category: **Gameplay Classes**
  28
  29    A single instance of this is shared between all bombs
  30    and can be retrieved via bastd.actor.bomb.get_factory().
  31    """
  32
  33    bomb_model: ba.Model
  34    """The ba.Model of a standard or ice bomb."""
  35
  36    sticky_bomb_model: ba.Model
  37    """The ba.Model of a sticky-bomb."""
  38
  39    impact_bomb_model: ba.Model
  40    """The ba.Model of an impact-bomb."""
  41
  42    land_mine_model: ba.Model
  43    """The ba.Model of a land-mine."""
  44
  45    tnt_model: ba.Model
  46    """The ba.Model of a tnt box."""
  47
  48    regular_tex: ba.Texture
  49    """The ba.Texture for regular bombs."""
  50
  51    ice_tex: ba.Texture
  52    """The ba.Texture for ice bombs."""
  53
  54    sticky_tex: ba.Texture
  55    """The ba.Texture for sticky bombs."""
  56
  57    impact_tex: ba.Texture
  58    """The ba.Texture for impact bombs."""
  59
  60    impact_lit_tex: ba.Texture
  61    """The ba.Texture for impact bombs with lights lit."""
  62
  63    land_mine_tex: ba.Texture
  64    """The ba.Texture for land-mines."""
  65
  66    land_mine_lit_tex: ba.Texture
  67    """The ba.Texture for land-mines with the light lit."""
  68
  69    tnt_tex: ba.Texture
  70    """The ba.Texture for tnt boxes."""
  71
  72    hiss_sound: ba.Sound
  73    """The ba.Sound for the hiss sound an ice bomb makes."""
  74
  75    debris_fall_sound: ba.Sound
  76    """The ba.Sound for random falling debris after an explosion."""
  77
  78    wood_debris_fall_sound: ba.Sound
  79    """A ba.Sound for random wood debris falling after an explosion."""
  80
  81    explode_sounds: Sequence[ba.Sound]
  82    """A tuple of ba.Sound-s for explosions."""
  83
  84    freeze_sound: ba.Sound
  85    """A ba.Sound of an ice bomb freezing something."""
  86
  87    fuse_sound: ba.Sound
  88    """A ba.Sound of a burning fuse."""
  89
  90    activate_sound: ba.Sound
  91    """A ba.Sound for an activating impact bomb."""
  92
  93    warn_sound: ba.Sound
  94    """A ba.Sound for an impact bomb about to explode due to time-out."""
  95
  96    bomb_material: ba.Material
  97    """A ba.Material applied to all bombs."""
  98
  99    normal_sound_material: ba.Material
 100    """A ba.Material that generates standard bomb noises on impacts, etc."""
 101
 102    sticky_material: ba.Material
 103    """A ba.Material that makes 'splat' sounds and makes collisions softer."""
 104
 105    land_mine_no_explode_material: ba.Material
 106    """A ba.Material that keeps land-mines from blowing up.
 107       Applied to land-mines when they are created to allow land-mines to
 108       touch without exploding."""
 109
 110    land_mine_blast_material: ba.Material
 111    """A ba.Material applied to activated land-mines that causes them to
 112       explode on impact."""
 113
 114    impact_blast_material: ba.Material
 115    """A ba.Material applied to activated impact-bombs that causes them to
 116       explode on impact."""
 117
 118    blast_material: ba.Material
 119    """A ba.Material applied to bomb blast geometry which triggers impact
 120       events with what it touches."""
 121
 122    dink_sounds: Sequence[ba.Sound]
 123    """A tuple of ba.Sound-s for when bombs hit the ground."""
 124
 125    sticky_impact_sound: ba.Sound
 126    """The ba.Sound for a squish made by a sticky bomb hitting something."""
 127
 128    roll_sound: ba.Sound
 129    """ba.Sound for a rolling bomb."""
 130
 131    _STORENAME = ba.storagename()
 132
 133    @classmethod
 134    def get(cls) -> BombFactory:
 135        """Get/create a shared bastd.actor.bomb.BombFactory object."""
 136        activity = ba.getactivity()
 137        factory = activity.customdata.get(cls._STORENAME)
 138        if factory is None:
 139            factory = BombFactory()
 140            activity.customdata[cls._STORENAME] = factory
 141        assert isinstance(factory, BombFactory)
 142        return factory
 143
 144    def random_explode_sound(self) -> ba.Sound:
 145        """Return a random explosion ba.Sound from the factory."""
 146        return self.explode_sounds[random.randrange(len(self.explode_sounds))]
 147
 148    def __init__(self) -> None:
 149        """Instantiate a BombFactory.
 150
 151        You shouldn't need to do this; call bastd.actor.bomb.get_factory()
 152        to get a shared instance.
 153        """
 154        shared = SharedObjects.get()
 155
 156        self.bomb_model = ba.getmodel('bomb')
 157        self.sticky_bomb_model = ba.getmodel('bombSticky')
 158        self.impact_bomb_model = ba.getmodel('impactBomb')
 159        self.land_mine_model = ba.getmodel('landMine')
 160        self.tnt_model = ba.getmodel('tnt')
 161
 162        self.regular_tex = ba.gettexture('bombColor')
 163        self.ice_tex = ba.gettexture('bombColorIce')
 164        self.sticky_tex = ba.gettexture('bombStickyColor')
 165        self.impact_tex = ba.gettexture('impactBombColor')
 166        self.impact_lit_tex = ba.gettexture('impactBombColorLit')
 167        self.land_mine_tex = ba.gettexture('landMine')
 168        self.land_mine_lit_tex = ba.gettexture('landMineLit')
 169        self.tnt_tex = ba.gettexture('tnt')
 170
 171        self.hiss_sound = ba.getsound('hiss')
 172        self.debris_fall_sound = ba.getsound('debrisFall')
 173        self.wood_debris_fall_sound = ba.getsound('woodDebrisFall')
 174
 175        self.explode_sounds = (ba.getsound('explosion01'),
 176                               ba.getsound('explosion02'),
 177                               ba.getsound('explosion03'),
 178                               ba.getsound('explosion04'),
 179                               ba.getsound('explosion05'))
 180
 181        self.freeze_sound = ba.getsound('freeze')
 182        self.fuse_sound = ba.getsound('fuse01')
 183        self.activate_sound = ba.getsound('activateBeep')
 184        self.warn_sound = ba.getsound('warnBeep')
 185
 186        # Set up our material so new bombs don't collide with objects
 187        # that they are initially overlapping.
 188        self.bomb_material = ba.Material()
 189        self.normal_sound_material = ba.Material()
 190        self.sticky_material = ba.Material()
 191
 192        self.bomb_material.add_actions(
 193            conditions=(
 194                (
 195                    ('we_are_younger_than', 100),
 196                    'or',
 197                    ('they_are_younger_than', 100),
 198                ),
 199                'and',
 200                ('they_have_material', shared.object_material),
 201            ),
 202            actions=('modify_node_collision', 'collide', False),
 203        )
 204
 205        # We want pickup materials to always hit us even if we're currently
 206        # not colliding with their node. (generally due to the above rule)
 207        self.bomb_material.add_actions(
 208            conditions=('they_have_material', shared.pickup_material),
 209            actions=('modify_part_collision', 'use_node_collide', False),
 210        )
 211
 212        self.bomb_material.add_actions(actions=('modify_part_collision',
 213                                                'friction', 0.3))
 214
 215        self.land_mine_no_explode_material = ba.Material()
 216        self.land_mine_blast_material = ba.Material()
 217        self.land_mine_blast_material.add_actions(
 218            conditions=(
 219                ('we_are_older_than', 200),
 220                'and',
 221                ('they_are_older_than', 200),
 222                'and',
 223                ('eval_colliding', ),
 224                'and',
 225                (
 226                    ('they_dont_have_material',
 227                     self.land_mine_no_explode_material),
 228                    'and',
 229                    (
 230                        ('they_have_material', shared.object_material),
 231                        'or',
 232                        ('they_have_material', shared.player_material),
 233                    ),
 234                ),
 235            ),
 236            actions=('message', 'our_node', 'at_connect', ImpactMessage()),
 237        )
 238
 239        self.impact_blast_material = ba.Material()
 240        self.impact_blast_material.add_actions(
 241            conditions=(
 242                ('we_are_older_than', 200),
 243                'and',
 244                ('they_are_older_than', 200),
 245                'and',
 246                ('eval_colliding', ),
 247                'and',
 248                (
 249                    ('they_have_material', shared.footing_material),
 250                    'or',
 251                    ('they_have_material', shared.object_material),
 252                ),
 253            ),
 254            actions=('message', 'our_node', 'at_connect', ImpactMessage()),
 255        )
 256
 257        self.blast_material = ba.Material()
 258        self.blast_material.add_actions(
 259            conditions=('they_have_material', shared.object_material),
 260            actions=(
 261                ('modify_part_collision', 'collide', True),
 262                ('modify_part_collision', 'physical', False),
 263                ('message', 'our_node', 'at_connect', ExplodeHitMessage()),
 264            ),
 265        )
 266
 267        self.dink_sounds = (ba.getsound('bombDrop01'),
 268                            ba.getsound('bombDrop02'))
 269        self.sticky_impact_sound = ba.getsound('stickyImpact')
 270        self.roll_sound = ba.getsound('bombRoll01')
 271
 272        # Collision sounds.
 273        self.normal_sound_material.add_actions(
 274            conditions=('they_have_material', shared.footing_material),
 275            actions=(
 276                ('impact_sound', self.dink_sounds, 2, 0.8),
 277                ('roll_sound', self.roll_sound, 3, 6),
 278            ))
 279
 280        self.sticky_material.add_actions(actions=(('modify_part_collision',
 281                                                   'stiffness', 0.1),
 282                                                  ('modify_part_collision',
 283                                                   'damping', 1.0)))
 284
 285        self.sticky_material.add_actions(
 286            conditions=(
 287                ('they_have_material', shared.player_material),
 288                'or',
 289                ('they_have_material', shared.footing_material),
 290            ),
 291            actions=('message', 'our_node', 'at_connect', SplatMessage()),
 292        )
 293
 294
 295class SplatMessage:
 296    """Tells an object to make a splat noise."""
 297
 298
 299class ExplodeMessage:
 300    """Tells an object to explode."""
 301
 302
 303class ImpactMessage:
 304    """Tell an object it touched something."""
 305
 306
 307class ArmMessage:
 308    """Tell an object to become armed."""
 309
 310
 311class WarnMessage:
 312    """Tell an object to issue a warning sound."""
 313
 314
 315class ExplodeHitMessage:
 316    """Tell an object it was hit by an explosion."""
 317
 318
 319class Blast(ba.Actor):
 320    """An explosion, as generated by a bomb or some other object.
 321
 322    category: Gameplay Classes
 323    """
 324
 325    def __init__(self,
 326                 position: Sequence[float] = (0.0, 1.0, 0.0),
 327                 velocity: Sequence[float] = (0.0, 0.0, 0.0),
 328                 blast_radius: float = 2.0,
 329                 blast_type: str = 'normal',
 330                 source_player: ba.Player | None = None,
 331                 hit_type: str = 'explosion',
 332                 hit_subtype: str = 'normal'):
 333        """Instantiate with given values."""
 334
 335        # bah; get off my lawn!
 336        # pylint: disable=too-many-locals
 337        # pylint: disable=too-many-statements
 338
 339        super().__init__()
 340
 341        shared = SharedObjects.get()
 342        factory = BombFactory.get()
 343
 344        self.blast_type = blast_type
 345        self._source_player = source_player
 346        self.hit_type = hit_type
 347        self.hit_subtype = hit_subtype
 348        self.radius = blast_radius
 349
 350        # Set our position a bit lower so we throw more things upward.
 351        rmats = (factory.blast_material, shared.attack_material)
 352        self.node = ba.newnode(
 353            'region',
 354            delegate=self,
 355            attrs={
 356                'position': (position[0], position[1] - 0.1, position[2]),
 357                'scale': (self.radius, self.radius, self.radius),
 358                'type': 'sphere',
 359                'materials': rmats
 360            },
 361        )
 362
 363        ba.timer(0.05, self.node.delete)
 364
 365        # Throw in an explosion and flash.
 366        evel = (velocity[0], max(-1.0, velocity[1]), velocity[2])
 367        explosion = ba.newnode('explosion',
 368                               attrs={
 369                                   'position': position,
 370                                   'velocity': evel,
 371                                   'radius': self.radius,
 372                                   'big': (self.blast_type == 'tnt')
 373                               })
 374        if self.blast_type == 'ice':
 375            explosion.color = (0, 0.05, 0.4)
 376
 377        ba.timer(1.0, explosion.delete)
 378
 379        if self.blast_type != 'ice':
 380            ba.emitfx(position=position,
 381                      velocity=velocity,
 382                      count=int(1.0 + random.random() * 4),
 383                      emit_type='tendrils',
 384                      tendril_type='thin_smoke')
 385        ba.emitfx(position=position,
 386                  velocity=velocity,
 387                  count=int(4.0 + random.random() * 4),
 388                  emit_type='tendrils',
 389                  tendril_type='ice' if self.blast_type == 'ice' else 'smoke')
 390        ba.emitfx(position=position,
 391                  emit_type='distortion',
 392                  spread=1.0 if self.blast_type == 'tnt' else 2.0)
 393
 394        # And emit some shrapnel.
 395        if self.blast_type == 'ice':
 396
 397            def emit() -> None:
 398                ba.emitfx(position=position,
 399                          velocity=velocity,
 400                          count=30,
 401                          spread=2.0,
 402                          scale=0.4,
 403                          chunk_type='ice',
 404                          emit_type='stickers')
 405
 406            # It looks better if we delay a bit.
 407            ba.timer(0.05, emit)
 408
 409        elif self.blast_type == 'sticky':
 410
 411            def emit() -> None:
 412                ba.emitfx(position=position,
 413                          velocity=velocity,
 414                          count=int(4.0 + random.random() * 8),
 415                          spread=0.7,
 416                          chunk_type='slime')
 417                ba.emitfx(position=position,
 418                          velocity=velocity,
 419                          count=int(4.0 + random.random() * 8),
 420                          scale=0.5,
 421                          spread=0.7,
 422                          chunk_type='slime')
 423                ba.emitfx(position=position,
 424                          velocity=velocity,
 425                          count=15,
 426                          scale=0.6,
 427                          chunk_type='slime',
 428                          emit_type='stickers')
 429                ba.emitfx(position=position,
 430                          velocity=velocity,
 431                          count=20,
 432                          scale=0.7,
 433                          chunk_type='spark',
 434                          emit_type='stickers')
 435                ba.emitfx(position=position,
 436                          velocity=velocity,
 437                          count=int(6.0 + random.random() * 12),
 438                          scale=0.8,
 439                          spread=1.5,
 440                          chunk_type='spark')
 441
 442            # It looks better if we delay a bit.
 443            ba.timer(0.05, emit)
 444
 445        elif self.blast_type == 'impact':
 446
 447            def emit() -> None:
 448                ba.emitfx(position=position,
 449                          velocity=velocity,
 450                          count=int(4.0 + random.random() * 8),
 451                          scale=0.8,
 452                          chunk_type='metal')
 453                ba.emitfx(position=position,
 454                          velocity=velocity,
 455                          count=int(4.0 + random.random() * 8),
 456                          scale=0.4,
 457                          chunk_type='metal')
 458                ba.emitfx(position=position,
 459                          velocity=velocity,
 460                          count=20,
 461                          scale=0.7,
 462                          chunk_type='spark',
 463                          emit_type='stickers')
 464                ba.emitfx(position=position,
 465                          velocity=velocity,
 466                          count=int(8.0 + random.random() * 15),
 467                          scale=0.8,
 468                          spread=1.5,
 469                          chunk_type='spark')
 470
 471            # It looks better if we delay a bit.
 472            ba.timer(0.05, emit)
 473
 474        else:  # Regular or land mine bomb shrapnel.
 475
 476            def emit() -> None:
 477                if self.blast_type != 'tnt':
 478                    ba.emitfx(position=position,
 479                              velocity=velocity,
 480                              count=int(4.0 + random.random() * 8),
 481                              chunk_type='rock')
 482                    ba.emitfx(position=position,
 483                              velocity=velocity,
 484                              count=int(4.0 + random.random() * 8),
 485                              scale=0.5,
 486                              chunk_type='rock')
 487                ba.emitfx(position=position,
 488                          velocity=velocity,
 489                          count=30,
 490                          scale=1.0 if self.blast_type == 'tnt' else 0.7,
 491                          chunk_type='spark',
 492                          emit_type='stickers')
 493                ba.emitfx(position=position,
 494                          velocity=velocity,
 495                          count=int(18.0 + random.random() * 20),
 496                          scale=1.0 if self.blast_type == 'tnt' else 0.8,
 497                          spread=1.5,
 498                          chunk_type='spark')
 499
 500                # TNT throws splintery chunks.
 501                if self.blast_type == 'tnt':
 502
 503                    def emit_splinters() -> None:
 504                        ba.emitfx(position=position,
 505                                  velocity=velocity,
 506                                  count=int(20.0 + random.random() * 25),
 507                                  scale=0.8,
 508                                  spread=1.0,
 509                                  chunk_type='splinter')
 510
 511                    ba.timer(0.01, emit_splinters)
 512
 513                # Every now and then do a sparky one.
 514                if self.blast_type == 'tnt' or random.random() < 0.1:
 515
 516                    def emit_extra_sparks() -> None:
 517                        ba.emitfx(position=position,
 518                                  velocity=velocity,
 519                                  count=int(10.0 + random.random() * 20),
 520                                  scale=0.8,
 521                                  spread=1.5,
 522                                  chunk_type='spark')
 523
 524                    ba.timer(0.02, emit_extra_sparks)
 525
 526            # It looks better if we delay a bit.
 527            ba.timer(0.05, emit)
 528
 529        lcolor = ((0.6, 0.6, 1.0) if self.blast_type == 'ice' else
 530                  (1, 0.3, 0.1))
 531        light = ba.newnode('light',
 532                           attrs={
 533                               'position': position,
 534                               'volume_intensity_scale': 10.0,
 535                               'color': lcolor
 536                           })
 537
 538        scl = random.uniform(0.6, 0.9)
 539        scorch_radius = light_radius = self.radius
 540        if self.blast_type == 'tnt':
 541            light_radius *= 1.4
 542            scorch_radius *= 1.15
 543            scl *= 3.0
 544
 545        iscale = 1.6
 546        ba.animate(
 547            light, 'intensity', {
 548                0: 2.0 * iscale,
 549                scl * 0.02: 0.1 * iscale,
 550                scl * 0.025: 0.2 * iscale,
 551                scl * 0.05: 17.0 * iscale,
 552                scl * 0.06: 5.0 * iscale,
 553                scl * 0.08: 4.0 * iscale,
 554                scl * 0.2: 0.6 * iscale,
 555                scl * 2.0: 0.00 * iscale,
 556                scl * 3.0: 0.0
 557            })
 558        ba.animate(
 559            light, 'radius', {
 560                0: light_radius * 0.2,
 561                scl * 0.05: light_radius * 0.55,
 562                scl * 0.1: light_radius * 0.3,
 563                scl * 0.3: light_radius * 0.15,
 564                scl * 1.0: light_radius * 0.05
 565            })
 566        ba.timer(scl * 3.0, light.delete)
 567
 568        # Make a scorch that fades over time.
 569        scorch = ba.newnode('scorch',
 570                            attrs={
 571                                'position': position,
 572                                'size': scorch_radius * 0.5,
 573                                'big': (self.blast_type == 'tnt')
 574                            })
 575        if self.blast_type == 'ice':
 576            scorch.color = (1, 1, 1.5)
 577
 578        ba.animate(scorch, 'presence', {3.000: 1, 13.000: 0})
 579        ba.timer(13.0, scorch.delete)
 580
 581        if self.blast_type == 'ice':
 582            ba.playsound(factory.hiss_sound, position=light.position)
 583
 584        lpos = light.position
 585        ba.playsound(factory.random_explode_sound(), position=lpos)
 586        ba.playsound(factory.debris_fall_sound, position=lpos)
 587
 588        ba.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0)
 589
 590        # TNT is more epic.
 591        if self.blast_type == 'tnt':
 592            ba.playsound(factory.random_explode_sound(), position=lpos)
 593
 594            def _extra_boom() -> None:
 595                ba.playsound(factory.random_explode_sound(), position=lpos)
 596
 597            ba.timer(0.25, _extra_boom)
 598
 599            def _extra_debris_sound() -> None:
 600                ba.playsound(factory.debris_fall_sound, position=lpos)
 601                ba.playsound(factory.wood_debris_fall_sound, position=lpos)
 602
 603            ba.timer(0.4, _extra_debris_sound)
 604
 605    def handlemessage(self, msg: Any) -> Any:
 606        assert not self.expired
 607
 608        if isinstance(msg, ba.DieMessage):
 609            if self.node:
 610                self.node.delete()
 611
 612        elif isinstance(msg, ExplodeHitMessage):
 613            node = ba.getcollision().opposingnode
 614            assert self.node
 615            nodepos = self.node.position
 616            mag = 2000.0
 617            if self.blast_type == 'ice':
 618                mag *= 0.5
 619            elif self.blast_type == 'land_mine':
 620                mag *= 2.5
 621            elif self.blast_type == 'tnt':
 622                mag *= 2.0
 623
 624            node.handlemessage(
 625                ba.HitMessage(pos=nodepos,
 626                              velocity=(0, 0, 0),
 627                              magnitude=mag,
 628                              hit_type=self.hit_type,
 629                              hit_subtype=self.hit_subtype,
 630                              radius=self.radius,
 631                              source_player=ba.existing(self._source_player)))
 632            if self.blast_type == 'ice':
 633                ba.playsound(BombFactory.get().freeze_sound,
 634                             10,
 635                             position=nodepos)
 636                node.handlemessage(ba.FreezeMessage())
 637
 638        else:
 639            return super().handlemessage(msg)
 640        return None
 641
 642
 643class Bomb(ba.Actor):
 644    """A standard bomb and its variants such as land-mines and tnt-boxes.
 645
 646    category: Gameplay Classes
 647    """
 648
 649    # Ew; should try to clean this up later.
 650    # pylint: disable=too-many-locals
 651    # pylint: disable=too-many-branches
 652    # pylint: disable=too-many-statements
 653
 654    def __init__(self,
 655                 position: Sequence[float] = (0.0, 1.0, 0.0),
 656                 velocity: Sequence[float] = (0.0, 0.0, 0.0),
 657                 bomb_type: str = 'normal',
 658                 blast_radius: float = 2.0,
 659                 bomb_scale: float = 1.0,
 660                 source_player: ba.Player | None = None,
 661                 owner: ba.Node | None = None):
 662        """Create a new Bomb.
 663
 664        bomb_type can be 'ice','impact','land_mine','normal','sticky', or
 665        'tnt'. Note that for impact or land_mine bombs you have to call arm()
 666        before they will go off.
 667        """
 668        super().__init__()
 669
 670        shared = SharedObjects.get()
 671        factory = BombFactory.get()
 672
 673        if bomb_type not in ('ice', 'impact', 'land_mine', 'normal', 'sticky',
 674                             'tnt'):
 675            raise ValueError('invalid bomb type: ' + bomb_type)
 676        self.bomb_type = bomb_type
 677
 678        self._exploded = False
 679        self.scale = bomb_scale
 680
 681        self.texture_sequence: ba.Node | None = None
 682
 683        if self.bomb_type == 'sticky':
 684            self._last_sticky_sound_time = 0.0
 685
 686        self.blast_radius = blast_radius
 687        if self.bomb_type == 'ice':
 688            self.blast_radius *= 1.2
 689        elif self.bomb_type == 'impact':
 690            self.blast_radius *= 0.7
 691        elif self.bomb_type == 'land_mine':
 692            self.blast_radius *= 0.7
 693        elif self.bomb_type == 'tnt':
 694            self.blast_radius *= 1.45
 695
 696        self._explode_callbacks: list[Callable[[Bomb, Blast], Any]] = []
 697
 698        # The player this came from.
 699        self._source_player = source_player
 700
 701        # By default our hit type/subtype is our own, but we pick up types of
 702        # whoever sets us off so we know what caused a chain reaction.
 703        # UPDATE (July 2020): not inheriting hit-types anymore; this causes
 704        # weird effects such as land-mines inheriting 'punch' hit types and
 705        # then not being able to destroy certain things they normally could,
 706        # etc. Inheriting owner/source-node from things that set us off
 707        # should be all we need I think...
 708        self.hit_type = 'explosion'
 709        self.hit_subtype = self.bomb_type
 710
 711        # The node this came from.
 712        # FIXME: can we unify this and source_player?
 713        self.owner = owner
 714
 715        # Adding footing-materials to things can screw up jumping and flying
 716        # since players carrying those things and thus touching footing
 717        # objects will think they're on solid ground.. perhaps we don't
 718        # wanna add this even in the tnt case?
 719        materials: tuple[ba.Material, ...]
 720        if self.bomb_type == 'tnt':
 721            materials = (factory.bomb_material, shared.footing_material,
 722                         shared.object_material)
 723        else:
 724            materials = (factory.bomb_material, shared.object_material)
 725
 726        if self.bomb_type == 'impact':
 727            materials = materials + (factory.impact_blast_material, )
 728        elif self.bomb_type == 'land_mine':
 729            materials = materials + (factory.land_mine_no_explode_material, )
 730
 731        if self.bomb_type == 'sticky':
 732            materials = materials + (factory.sticky_material, )
 733        else:
 734            materials = materials + (factory.normal_sound_material, )
 735
 736        if self.bomb_type == 'land_mine':
 737            fuse_time = None
 738            self.node = ba.newnode('prop',
 739                                   delegate=self,
 740                                   attrs={
 741                                       'position': position,
 742                                       'velocity': velocity,
 743                                       'model': factory.land_mine_model,
 744                                       'light_model': factory.land_mine_model,
 745                                       'body': 'landMine',
 746                                       'body_scale': self.scale,
 747                                       'shadow_size': 0.44,
 748                                       'color_texture': factory.land_mine_tex,
 749                                       'reflection': 'powerup',
 750                                       'reflection_scale': [1.0],
 751                                       'materials': materials
 752                                   })
 753
 754        elif self.bomb_type == 'tnt':
 755            fuse_time = None
 756            self.node = ba.newnode('prop',
 757                                   delegate=self,
 758                                   attrs={
 759                                       'position': position,
 760                                       'velocity': velocity,
 761                                       'model': factory.tnt_model,
 762                                       'light_model': factory.tnt_model,
 763                                       'body': 'crate',
 764                                       'body_scale': self.scale,
 765                                       'shadow_size': 0.5,
 766                                       'color_texture': factory.tnt_tex,
 767                                       'reflection': 'soft',
 768                                       'reflection_scale': [0.23],
 769                                       'materials': materials
 770                                   })
 771
 772        elif self.bomb_type == 'impact':
 773            fuse_time = 20.0
 774            self.node = ba.newnode('prop',
 775                                   delegate=self,
 776                                   attrs={
 777                                       'position': position,
 778                                       'velocity': velocity,
 779                                       'body': 'sphere',
 780                                       'body_scale': self.scale,
 781                                       'model': factory.impact_bomb_model,
 782                                       'shadow_size': 0.3,
 783                                       'color_texture': factory.impact_tex,
 784                                       'reflection': 'powerup',
 785                                       'reflection_scale': [1.5],
 786                                       'materials': materials
 787                                   })
 788            self.arm_timer = ba.Timer(
 789                0.2, ba.WeakCall(self.handlemessage, ArmMessage()))
 790            self.warn_timer = ba.Timer(
 791                fuse_time - 1.7, ba.WeakCall(self.handlemessage,
 792                                             WarnMessage()))
 793
 794        else:
 795            fuse_time = 3.0
 796            if self.bomb_type == 'sticky':
 797                sticky = True
 798                model = factory.sticky_bomb_model
 799                rtype = 'sharper'
 800                rscale = 1.8
 801            else:
 802                sticky = False
 803                model = factory.bomb_model
 804                rtype = 'sharper'
 805                rscale = 1.8
 806            if self.bomb_type == 'ice':
 807                tex = factory.ice_tex
 808            elif self.bomb_type == 'sticky':
 809                tex = factory.sticky_tex
 810            else:
 811                tex = factory.regular_tex
 812            self.node = ba.newnode('bomb',
 813                                   delegate=self,
 814                                   attrs={
 815                                       'position': position,
 816                                       'velocity': velocity,
 817                                       'model': model,
 818                                       'body_scale': self.scale,
 819                                       'shadow_size': 0.3,
 820                                       'color_texture': tex,
 821                                       'sticky': sticky,
 822                                       'owner': owner,
 823                                       'reflection': rtype,
 824                                       'reflection_scale': [rscale],
 825                                       'materials': materials
 826                                   })
 827
 828            sound = ba.newnode('sound',
 829                               owner=self.node,
 830                               attrs={
 831                                   'sound': factory.fuse_sound,
 832                                   'volume': 0.25
 833                               })
 834            self.node.connectattr('position', sound, 'position')
 835            ba.animate(self.node, 'fuse_length', {0.0: 1.0, fuse_time: 0.0})
 836
 837        # Light the fuse!!!
 838        if self.bomb_type not in ('land_mine', 'tnt'):
 839            assert fuse_time is not None
 840            ba.timer(fuse_time,
 841                     ba.WeakCall(self.handlemessage, ExplodeMessage()))
 842
 843        ba.animate(self.node, 'model_scale', {
 844            0: 0,
 845            0.2: 1.3 * self.scale,
 846            0.26: self.scale
 847        })
 848
 849    def get_source_player(self,
 850                          playertype: type[PlayerType]) -> PlayerType | None:
 851        """Return the source-player if one exists and is the provided type."""
 852        player: Any = self._source_player
 853        return (player if isinstance(player, playertype) and player.exists()
 854                else None)
 855
 856    def on_expire(self) -> None:
 857        super().on_expire()
 858
 859        # Release callbacks/refs so we don't wind up with dependency loops.
 860        self._explode_callbacks = []
 861
 862    def _handle_die(self) -> None:
 863        if self.node:
 864            self.node.delete()
 865
 866    def _handle_oob(self) -> None:
 867        self.handlemessage(ba.DieMessage())
 868
 869    def _handle_impact(self) -> None:
 870        node = ba.getcollision().opposingnode
 871
 872        # If we're an impact bomb and we came from this node, don't explode.
 873        # (otherwise we blow up on our own head when jumping).
 874        # Alternately if we're hitting another impact-bomb from the same
 875        # source, don't explode. (can cause accidental explosions if rapidly
 876        # throwing/etc.)
 877        node_delegate = node.getdelegate(object)
 878        if node:
 879            if (self.bomb_type == 'impact' and
 880                (node is self.owner or
 881                 (isinstance(node_delegate, Bomb) and node_delegate.bomb_type
 882                  == 'impact' and node_delegate.owner is self.owner))):
 883                return
 884            self.handlemessage(ExplodeMessage())
 885
 886    def _handle_dropped(self) -> None:
 887        if self.bomb_type == 'land_mine':
 888            self.arm_timer = ba.Timer(
 889                1.25, ba.WeakCall(self.handlemessage, ArmMessage()))
 890
 891        # Once we've thrown a sticky bomb we can stick to it.
 892        elif self.bomb_type == 'sticky':
 893
 894            def _setsticky(node: ba.Node) -> None:
 895                if node:
 896                    node.stick_to_owner = True
 897
 898            ba.timer(0.25, lambda: _setsticky(self.node))
 899
 900    def _handle_splat(self) -> None:
 901        node = ba.getcollision().opposingnode
 902        if (node is not self.owner
 903                and ba.time() - self._last_sticky_sound_time > 1.0):
 904            self._last_sticky_sound_time = ba.time()
 905            assert self.node
 906            ba.playsound(BombFactory.get().sticky_impact_sound,
 907                         2.0,
 908                         position=self.node.position)
 909
 910    def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None:
 911        """Add a call to be run when the bomb has exploded.
 912
 913        The bomb and the new blast object are passed as arguments.
 914        """
 915        self._explode_callbacks.append(call)
 916
 917    def explode(self) -> None:
 918        """Blows up the bomb if it has not yet done so."""
 919        if self._exploded:
 920            return
 921        self._exploded = True
 922        if self.node:
 923            blast = Blast(position=self.node.position,
 924                          velocity=self.node.velocity,
 925                          blast_radius=self.blast_radius,
 926                          blast_type=self.bomb_type,
 927                          source_player=ba.existing(self._source_player),
 928                          hit_type=self.hit_type,
 929                          hit_subtype=self.hit_subtype).autoretain()
 930            for callback in self._explode_callbacks:
 931                callback(self, blast)
 932
 933        # We blew up so we need to go away.
 934        # NOTE TO SELF: do we actually need this delay?
 935        ba.timer(0.001, ba.WeakCall(self.handlemessage, ba.DieMessage()))
 936
 937    def _handle_warn(self) -> None:
 938        if self.texture_sequence and self.node:
 939            self.texture_sequence.rate = 30
 940            ba.playsound(BombFactory.get().warn_sound,
 941                         0.5,
 942                         position=self.node.position)
 943
 944    def _add_material(self, material: ba.Material) -> None:
 945        if not self.node:
 946            return
 947        materials = self.node.materials
 948        if material not in materials:
 949            assert isinstance(materials, tuple)
 950            self.node.materials = materials + (material, )
 951
 952    def arm(self) -> None:
 953        """Arm the bomb (for land-mines and impact-bombs).
 954
 955        These types of bombs will not explode until they have been armed.
 956        """
 957        if not self.node:
 958            return
 959        factory = BombFactory.get()
 960        intex: Sequence[ba.Texture]
 961        if self.bomb_type == 'land_mine':
 962            intex = (factory.land_mine_lit_tex, factory.land_mine_tex)
 963            self.texture_sequence = ba.newnode('texture_sequence',
 964                                               owner=self.node,
 965                                               attrs={
 966                                                   'rate': 30,
 967                                                   'input_textures': intex
 968                                               })
 969            ba.timer(0.5, self.texture_sequence.delete)
 970
 971            # We now make it explodable.
 972            ba.timer(
 973                0.25,
 974                ba.WeakCall(self._add_material,
 975                            factory.land_mine_blast_material))
 976        elif self.bomb_type == 'impact':
 977            intex = (factory.impact_lit_tex, factory.impact_tex,
 978                     factory.impact_tex)
 979            self.texture_sequence = ba.newnode('texture_sequence',
 980                                               owner=self.node,
 981                                               attrs={
 982                                                   'rate': 100,
 983                                                   'input_textures': intex
 984                                               })
 985            ba.timer(
 986                0.25,
 987                ba.WeakCall(self._add_material,
 988                            factory.land_mine_blast_material))
 989        else:
 990            raise Exception('arm() should only be called '
 991                            'on land-mines or impact bombs')
 992        self.texture_sequence.connectattr('output_texture', self.node,
 993                                          'color_texture')
 994        ba.playsound(factory.activate_sound, 0.5, position=self.node.position)
 995
 996    def _handle_hit(self, msg: ba.HitMessage) -> None:
 997        ispunched = (msg.srcnode and msg.srcnode.getnodetype() == 'spaz')
 998
 999        # Normal bombs are triggered by non-punch impacts;
1000        # impact-bombs by all impacts.
1001        if (not self._exploded and
1002            (not ispunched or self.bomb_type in ['impact', 'land_mine'])):
1003
1004            # Also lets change the owner of the bomb to whoever is setting
1005            # us off. (this way points for big chain reactions go to the
1006            # person causing them).
1007            source_player = msg.get_source_player(ba.Player)
1008            if source_player is not None:
1009                self._source_player = source_player
1010
1011                # Also inherit the hit type (if a landmine sets off by a bomb,
1012                # the credit should go to the mine)
1013                # the exception is TNT.  TNT always gets credit.
1014                # UPDATE (July 2020): not doing this anymore. Causes too much
1015                # weird logic such as bombs acting like punches. Holler if
1016                # anything is noticeably broken due to this.
1017                # if self.bomb_type != 'tnt':
1018                #     self.hit_type = msg.hit_type
1019                #     self.hit_subtype = msg.hit_subtype
1020
1021            ba.timer(0.1 + random.random() * 0.1,
1022                     ba.WeakCall(self.handlemessage, ExplodeMessage()))
1023        assert self.node
1024        self.node.handlemessage('impulse', msg.pos[0], msg.pos[1], msg.pos[2],
1025                                msg.velocity[0], msg.velocity[1],
1026                                msg.velocity[2], msg.magnitude,
1027                                msg.velocity_magnitude, msg.radius, 0,
1028                                msg.velocity[0], msg.velocity[1],
1029                                msg.velocity[2])
1030
1031        if msg.srcnode:
1032            pass
1033
1034    def handlemessage(self, msg: Any) -> Any:
1035        if isinstance(msg, ExplodeMessage):
1036            self.explode()
1037        elif isinstance(msg, ImpactMessage):
1038            self._handle_impact()
1039        # Ok the logic below looks like it was backwards to me.
1040        # Disabling for now; can bring back if need be.
1041        # elif isinstance(msg, ba.PickedUpMessage):
1042        #     # Change our source to whoever just picked us up *only* if it
1043        #     # is None. This way we can get points for killing bots with their
1044        #     # own bombs. Hmm would there be a downside to this?
1045        #     if self._source_player is not None:
1046        #         self._source_player = msg.node.source_player
1047        elif isinstance(msg, SplatMessage):
1048            self._handle_splat()
1049        elif isinstance(msg, ba.DroppedMessage):
1050            self._handle_dropped()
1051        elif isinstance(msg, ba.HitMessage):
1052            self._handle_hit(msg)
1053        elif isinstance(msg, ba.DieMessage):
1054            self._handle_die()
1055        elif isinstance(msg, ba.OutOfBoundsMessage):
1056            self._handle_oob()
1057        elif isinstance(msg, ArmMessage):
1058            self.arm()
1059        elif isinstance(msg, WarnMessage):
1060            self._handle_warn()
1061        else:
1062            super().handlemessage(msg)
1063
1064
1065class TNTSpawner:
1066    """Regenerates TNT at a given point in space every now and then.
1067
1068    category: Gameplay Classes
1069    """
1070
1071    def __init__(self, position: Sequence[float], respawn_time: float = 20.0):
1072        """Instantiate with given position and respawn_time (in seconds)."""
1073        self._position = position
1074        self._tnt: Bomb | None = None
1075        self._respawn_time = random.uniform(0.8, 1.2) * respawn_time
1076        self._wait_time = 0.0
1077        self._update()
1078
1079        # Go with slightly more than 1 second to avoid timer stacking.
1080        self._update_timer = ba.Timer(1.1,
1081                                      ba.WeakCall(self._update),
1082                                      repeat=True)
1083
1084    def _update(self) -> None:
1085        tnt_alive = self._tnt is not None and self._tnt.node
1086        if not tnt_alive:
1087            # Respawn if its been long enough.. otherwise just increment our
1088            # how-long-since-we-died value.
1089            if self._tnt is None or self._wait_time >= self._respawn_time:
1090                self._tnt = Bomb(position=self._position, bomb_type='tnt')
1091                self._wait_time = 0.0
1092            else:
1093                self._wait_time += 1.1
class BombFactory:
 25class BombFactory:
 26    """Wraps up media and other resources used by ba.Bombs.
 27
 28    Category: **Gameplay Classes**
 29
 30    A single instance of this is shared between all bombs
 31    and can be retrieved via bastd.actor.bomb.get_factory().
 32    """
 33
 34    bomb_model: ba.Model
 35    """The ba.Model of a standard or ice bomb."""
 36
 37    sticky_bomb_model: ba.Model
 38    """The ba.Model of a sticky-bomb."""
 39
 40    impact_bomb_model: ba.Model
 41    """The ba.Model of an impact-bomb."""
 42
 43    land_mine_model: ba.Model
 44    """The ba.Model of a land-mine."""
 45
 46    tnt_model: ba.Model
 47    """The ba.Model of a tnt box."""
 48
 49    regular_tex: ba.Texture
 50    """The ba.Texture for regular bombs."""
 51
 52    ice_tex: ba.Texture
 53    """The ba.Texture for ice bombs."""
 54
 55    sticky_tex: ba.Texture
 56    """The ba.Texture for sticky bombs."""
 57
 58    impact_tex: ba.Texture
 59    """The ba.Texture for impact bombs."""
 60
 61    impact_lit_tex: ba.Texture
 62    """The ba.Texture for impact bombs with lights lit."""
 63
 64    land_mine_tex: ba.Texture
 65    """The ba.Texture for land-mines."""
 66
 67    land_mine_lit_tex: ba.Texture
 68    """The ba.Texture for land-mines with the light lit."""
 69
 70    tnt_tex: ba.Texture
 71    """The ba.Texture for tnt boxes."""
 72
 73    hiss_sound: ba.Sound
 74    """The ba.Sound for the hiss sound an ice bomb makes."""
 75
 76    debris_fall_sound: ba.Sound
 77    """The ba.Sound for random falling debris after an explosion."""
 78
 79    wood_debris_fall_sound: ba.Sound
 80    """A ba.Sound for random wood debris falling after an explosion."""
 81
 82    explode_sounds: Sequence[ba.Sound]
 83    """A tuple of ba.Sound-s for explosions."""
 84
 85    freeze_sound: ba.Sound
 86    """A ba.Sound of an ice bomb freezing something."""
 87
 88    fuse_sound: ba.Sound
 89    """A ba.Sound of a burning fuse."""
 90
 91    activate_sound: ba.Sound
 92    """A ba.Sound for an activating impact bomb."""
 93
 94    warn_sound: ba.Sound
 95    """A ba.Sound for an impact bomb about to explode due to time-out."""
 96
 97    bomb_material: ba.Material
 98    """A ba.Material applied to all bombs."""
 99
100    normal_sound_material: ba.Material
101    """A ba.Material that generates standard bomb noises on impacts, etc."""
102
103    sticky_material: ba.Material
104    """A ba.Material that makes 'splat' sounds and makes collisions softer."""
105
106    land_mine_no_explode_material: ba.Material
107    """A ba.Material that keeps land-mines from blowing up.
108       Applied to land-mines when they are created to allow land-mines to
109       touch without exploding."""
110
111    land_mine_blast_material: ba.Material
112    """A ba.Material applied to activated land-mines that causes them to
113       explode on impact."""
114
115    impact_blast_material: ba.Material
116    """A ba.Material applied to activated impact-bombs that causes them to
117       explode on impact."""
118
119    blast_material: ba.Material
120    """A ba.Material applied to bomb blast geometry which triggers impact
121       events with what it touches."""
122
123    dink_sounds: Sequence[ba.Sound]
124    """A tuple of ba.Sound-s for when bombs hit the ground."""
125
126    sticky_impact_sound: ba.Sound
127    """The ba.Sound for a squish made by a sticky bomb hitting something."""
128
129    roll_sound: ba.Sound
130    """ba.Sound for a rolling bomb."""
131
132    _STORENAME = ba.storagename()
133
134    @classmethod
135    def get(cls) -> BombFactory:
136        """Get/create a shared bastd.actor.bomb.BombFactory object."""
137        activity = ba.getactivity()
138        factory = activity.customdata.get(cls._STORENAME)
139        if factory is None:
140            factory = BombFactory()
141            activity.customdata[cls._STORENAME] = factory
142        assert isinstance(factory, BombFactory)
143        return factory
144
145    def random_explode_sound(self) -> ba.Sound:
146        """Return a random explosion ba.Sound from the factory."""
147        return self.explode_sounds[random.randrange(len(self.explode_sounds))]
148
149    def __init__(self) -> None:
150        """Instantiate a BombFactory.
151
152        You shouldn't need to do this; call bastd.actor.bomb.get_factory()
153        to get a shared instance.
154        """
155        shared = SharedObjects.get()
156
157        self.bomb_model = ba.getmodel('bomb')
158        self.sticky_bomb_model = ba.getmodel('bombSticky')
159        self.impact_bomb_model = ba.getmodel('impactBomb')
160        self.land_mine_model = ba.getmodel('landMine')
161        self.tnt_model = ba.getmodel('tnt')
162
163        self.regular_tex = ba.gettexture('bombColor')
164        self.ice_tex = ba.gettexture('bombColorIce')
165        self.sticky_tex = ba.gettexture('bombStickyColor')
166        self.impact_tex = ba.gettexture('impactBombColor')
167        self.impact_lit_tex = ba.gettexture('impactBombColorLit')
168        self.land_mine_tex = ba.gettexture('landMine')
169        self.land_mine_lit_tex = ba.gettexture('landMineLit')
170        self.tnt_tex = ba.gettexture('tnt')
171
172        self.hiss_sound = ba.getsound('hiss')
173        self.debris_fall_sound = ba.getsound('debrisFall')
174        self.wood_debris_fall_sound = ba.getsound('woodDebrisFall')
175
176        self.explode_sounds = (ba.getsound('explosion01'),
177                               ba.getsound('explosion02'),
178                               ba.getsound('explosion03'),
179                               ba.getsound('explosion04'),
180                               ba.getsound('explosion05'))
181
182        self.freeze_sound = ba.getsound('freeze')
183        self.fuse_sound = ba.getsound('fuse01')
184        self.activate_sound = ba.getsound('activateBeep')
185        self.warn_sound = ba.getsound('warnBeep')
186
187        # Set up our material so new bombs don't collide with objects
188        # that they are initially overlapping.
189        self.bomb_material = ba.Material()
190        self.normal_sound_material = ba.Material()
191        self.sticky_material = ba.Material()
192
193        self.bomb_material.add_actions(
194            conditions=(
195                (
196                    ('we_are_younger_than', 100),
197                    'or',
198                    ('they_are_younger_than', 100),
199                ),
200                'and',
201                ('they_have_material', shared.object_material),
202            ),
203            actions=('modify_node_collision', 'collide', False),
204        )
205
206        # We want pickup materials to always hit us even if we're currently
207        # not colliding with their node. (generally due to the above rule)
208        self.bomb_material.add_actions(
209            conditions=('they_have_material', shared.pickup_material),
210            actions=('modify_part_collision', 'use_node_collide', False),
211        )
212
213        self.bomb_material.add_actions(actions=('modify_part_collision',
214                                                'friction', 0.3))
215
216        self.land_mine_no_explode_material = ba.Material()
217        self.land_mine_blast_material = ba.Material()
218        self.land_mine_blast_material.add_actions(
219            conditions=(
220                ('we_are_older_than', 200),
221                'and',
222                ('they_are_older_than', 200),
223                'and',
224                ('eval_colliding', ),
225                'and',
226                (
227                    ('they_dont_have_material',
228                     self.land_mine_no_explode_material),
229                    'and',
230                    (
231                        ('they_have_material', shared.object_material),
232                        'or',
233                        ('they_have_material', shared.player_material),
234                    ),
235                ),
236            ),
237            actions=('message', 'our_node', 'at_connect', ImpactMessage()),
238        )
239
240        self.impact_blast_material = ba.Material()
241        self.impact_blast_material.add_actions(
242            conditions=(
243                ('we_are_older_than', 200),
244                'and',
245                ('they_are_older_than', 200),
246                'and',
247                ('eval_colliding', ),
248                'and',
249                (
250                    ('they_have_material', shared.footing_material),
251                    'or',
252                    ('they_have_material', shared.object_material),
253                ),
254            ),
255            actions=('message', 'our_node', 'at_connect', ImpactMessage()),
256        )
257
258        self.blast_material = ba.Material()
259        self.blast_material.add_actions(
260            conditions=('they_have_material', shared.object_material),
261            actions=(
262                ('modify_part_collision', 'collide', True),
263                ('modify_part_collision', 'physical', False),
264                ('message', 'our_node', 'at_connect', ExplodeHitMessage()),
265            ),
266        )
267
268        self.dink_sounds = (ba.getsound('bombDrop01'),
269                            ba.getsound('bombDrop02'))
270        self.sticky_impact_sound = ba.getsound('stickyImpact')
271        self.roll_sound = ba.getsound('bombRoll01')
272
273        # Collision sounds.
274        self.normal_sound_material.add_actions(
275            conditions=('they_have_material', shared.footing_material),
276            actions=(
277                ('impact_sound', self.dink_sounds, 2, 0.8),
278                ('roll_sound', self.roll_sound, 3, 6),
279            ))
280
281        self.sticky_material.add_actions(actions=(('modify_part_collision',
282                                                   'stiffness', 0.1),
283                                                  ('modify_part_collision',
284                                                   'damping', 1.0)))
285
286        self.sticky_material.add_actions(
287            conditions=(
288                ('they_have_material', shared.player_material),
289                'or',
290                ('they_have_material', shared.footing_material),
291            ),
292            actions=('message', 'our_node', 'at_connect', SplatMessage()),
293        )

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().

BombFactory()
149    def __init__(self) -> None:
150        """Instantiate a BombFactory.
151
152        You shouldn't need to do this; call bastd.actor.bomb.get_factory()
153        to get a shared instance.
154        """
155        shared = SharedObjects.get()
156
157        self.bomb_model = ba.getmodel('bomb')
158        self.sticky_bomb_model = ba.getmodel('bombSticky')
159        self.impact_bomb_model = ba.getmodel('impactBomb')
160        self.land_mine_model = ba.getmodel('landMine')
161        self.tnt_model = ba.getmodel('tnt')
162
163        self.regular_tex = ba.gettexture('bombColor')
164        self.ice_tex = ba.gettexture('bombColorIce')
165        self.sticky_tex = ba.gettexture('bombStickyColor')
166        self.impact_tex = ba.gettexture('impactBombColor')
167        self.impact_lit_tex = ba.gettexture('impactBombColorLit')
168        self.land_mine_tex = ba.gettexture('landMine')
169        self.land_mine_lit_tex = ba.gettexture('landMineLit')
170        self.tnt_tex = ba.gettexture('tnt')
171
172        self.hiss_sound = ba.getsound('hiss')
173        self.debris_fall_sound = ba.getsound('debrisFall')
174        self.wood_debris_fall_sound = ba.getsound('woodDebrisFall')
175
176        self.explode_sounds = (ba.getsound('explosion01'),
177                               ba.getsound('explosion02'),
178                               ba.getsound('explosion03'),
179                               ba.getsound('explosion04'),
180                               ba.getsound('explosion05'))
181
182        self.freeze_sound = ba.getsound('freeze')
183        self.fuse_sound = ba.getsound('fuse01')
184        self.activate_sound = ba.getsound('activateBeep')
185        self.warn_sound = ba.getsound('warnBeep')
186
187        # Set up our material so new bombs don't collide with objects
188        # that they are initially overlapping.
189        self.bomb_material = ba.Material()
190        self.normal_sound_material = ba.Material()
191        self.sticky_material = ba.Material()
192
193        self.bomb_material.add_actions(
194            conditions=(
195                (
196                    ('we_are_younger_than', 100),
197                    'or',
198                    ('they_are_younger_than', 100),
199                ),
200                'and',
201                ('they_have_material', shared.object_material),
202            ),
203            actions=('modify_node_collision', 'collide', False),
204        )
205
206        # We want pickup materials to always hit us even if we're currently
207        # not colliding with their node. (generally due to the above rule)
208        self.bomb_material.add_actions(
209            conditions=('they_have_material', shared.pickup_material),
210            actions=('modify_part_collision', 'use_node_collide', False),
211        )
212
213        self.bomb_material.add_actions(actions=('modify_part_collision',
214                                                'friction', 0.3))
215
216        self.land_mine_no_explode_material = ba.Material()
217        self.land_mine_blast_material = ba.Material()
218        self.land_mine_blast_material.add_actions(
219            conditions=(
220                ('we_are_older_than', 200),
221                'and',
222                ('they_are_older_than', 200),
223                'and',
224                ('eval_colliding', ),
225                'and',
226                (
227                    ('they_dont_have_material',
228                     self.land_mine_no_explode_material),
229                    'and',
230                    (
231                        ('they_have_material', shared.object_material),
232                        'or',
233                        ('they_have_material', shared.player_material),
234                    ),
235                ),
236            ),
237            actions=('message', 'our_node', 'at_connect', ImpactMessage()),
238        )
239
240        self.impact_blast_material = ba.Material()
241        self.impact_blast_material.add_actions(
242            conditions=(
243                ('we_are_older_than', 200),
244                'and',
245                ('they_are_older_than', 200),
246                'and',
247                ('eval_colliding', ),
248                'and',
249                (
250                    ('they_have_material', shared.footing_material),
251                    'or',
252                    ('they_have_material', shared.object_material),
253                ),
254            ),
255            actions=('message', 'our_node', 'at_connect', ImpactMessage()),
256        )
257
258        self.blast_material = ba.Material()
259        self.blast_material.add_actions(
260            conditions=('they_have_material', shared.object_material),
261            actions=(
262                ('modify_part_collision', 'collide', True),
263                ('modify_part_collision', 'physical', False),
264                ('message', 'our_node', 'at_connect', ExplodeHitMessage()),
265            ),
266        )
267
268        self.dink_sounds = (ba.getsound('bombDrop01'),
269                            ba.getsound('bombDrop02'))
270        self.sticky_impact_sound = ba.getsound('stickyImpact')
271        self.roll_sound = ba.getsound('bombRoll01')
272
273        # Collision sounds.
274        self.normal_sound_material.add_actions(
275            conditions=('they_have_material', shared.footing_material),
276            actions=(
277                ('impact_sound', self.dink_sounds, 2, 0.8),
278                ('roll_sound', self.roll_sound, 3, 6),
279            ))
280
281        self.sticky_material.add_actions(actions=(('modify_part_collision',
282                                                   'stiffness', 0.1),
283                                                  ('modify_part_collision',
284                                                   'damping', 1.0)))
285
286        self.sticky_material.add_actions(
287            conditions=(
288                ('they_have_material', shared.player_material),
289                'or',
290                ('they_have_material', shared.footing_material),
291            ),
292            actions=('message', 'our_node', 'at_connect', SplatMessage()),
293        )

Instantiate a BombFactory.

You shouldn't need to do this; call bastd.actor.bomb.get_factory() to get a shared instance.

bomb_model: _ba.Model

The ba.Model of a standard or ice bomb.

sticky_bomb_model: _ba.Model

The ba.Model of a sticky-bomb.

impact_bomb_model: _ba.Model

The ba.Model of an impact-bomb.

land_mine_model: _ba.Model

The ba.Model of a land-mine.

tnt_model: _ba.Model

The ba.Model of a tnt box.

regular_tex: _ba.Texture

The ba.Texture for regular bombs.

ice_tex: _ba.Texture

The ba.Texture for ice bombs.

sticky_tex: _ba.Texture

The ba.Texture for sticky bombs.

impact_tex: _ba.Texture

The ba.Texture for impact bombs.

impact_lit_tex: _ba.Texture

The ba.Texture for impact bombs with lights lit.

land_mine_tex: _ba.Texture

The ba.Texture for land-mines.

land_mine_lit_tex: _ba.Texture

The ba.Texture for land-mines with the light lit.

tnt_tex: _ba.Texture

The ba.Texture for tnt boxes.

hiss_sound: _ba.Sound

The ba.Sound for the hiss sound an ice bomb makes.

debris_fall_sound: _ba.Sound

The ba.Sound for random falling debris after an explosion.

wood_debris_fall_sound: _ba.Sound

A ba.Sound for random wood debris falling after an explosion.

explode_sounds: Sequence[_ba.Sound]

A tuple of ba.Sound-s for explosions.

freeze_sound: _ba.Sound

A ba.Sound of an ice bomb freezing something.

fuse_sound: _ba.Sound

A ba.Sound of a burning fuse.

activate_sound: _ba.Sound

A ba.Sound for an activating impact bomb.

warn_sound: _ba.Sound

A ba.Sound for an impact bomb about to explode due to time-out.

bomb_material: _ba.Material

A ba.Material applied to all bombs.

normal_sound_material: _ba.Material

A ba.Material that generates standard bomb noises on impacts, etc.

sticky_material: _ba.Material

A ba.Material that makes 'splat' sounds and makes collisions softer.

land_mine_no_explode_material: _ba.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: _ba.Material

A ba.Material applied to activated land-mines that causes them to explode on impact.

impact_blast_material: _ba.Material

A ba.Material applied to activated impact-bombs that causes them to explode on impact.

blast_material: _ba.Material

A ba.Material applied to bomb blast geometry which triggers impact events with what it touches.

dink_sounds: Sequence[_ba.Sound]

A tuple of ba.Sound-s for when bombs hit the ground.

sticky_impact_sound: _ba.Sound

The ba.Sound for a squish made by a sticky bomb hitting something.

roll_sound: _ba.Sound

ba.Sound for a rolling bomb.

@classmethod
def get(cls) -> bastd.actor.bomb.BombFactory:
134    @classmethod
135    def get(cls) -> BombFactory:
136        """Get/create a shared bastd.actor.bomb.BombFactory object."""
137        activity = ba.getactivity()
138        factory = activity.customdata.get(cls._STORENAME)
139        if factory is None:
140            factory = BombFactory()
141            activity.customdata[cls._STORENAME] = factory
142        assert isinstance(factory, BombFactory)
143        return factory

Get/create a shared bastd.actor.bomb.BombFactory object.

def random_explode_sound(self) -> _ba.Sound:
145    def random_explode_sound(self) -> ba.Sound:
146        """Return a random explosion ba.Sound from the factory."""
147        return self.explode_sounds[random.randrange(len(self.explode_sounds))]

Return a random explosion ba.Sound from the factory.

class SplatMessage:
296class SplatMessage:
297    """Tells an object to make a splat noise."""

Tells an object to make a splat noise.

SplatMessage()
class ExplodeMessage:
300class ExplodeMessage:
301    """Tells an object to explode."""

Tells an object to explode.

ExplodeMessage()
class ImpactMessage:
304class ImpactMessage:
305    """Tell an object it touched something."""

Tell an object it touched something.

ImpactMessage()
class ArmMessage:
308class ArmMessage:
309    """Tell an object to become armed."""

Tell an object to become armed.

ArmMessage()
class WarnMessage:
312class WarnMessage:
313    """Tell an object to issue a warning sound."""

Tell an object to issue a warning sound.

WarnMessage()
class ExplodeHitMessage:
316class ExplodeHitMessage:
317    """Tell an object it was hit by an explosion."""

Tell an object it was hit by an explosion.

ExplodeHitMessage()
class Blast(ba._actor.Actor):
320class Blast(ba.Actor):
321    """An explosion, as generated by a bomb or some other object.
322
323    category: Gameplay Classes
324    """
325
326    def __init__(self,
327                 position: Sequence[float] = (0.0, 1.0, 0.0),
328                 velocity: Sequence[float] = (0.0, 0.0, 0.0),
329                 blast_radius: float = 2.0,
330                 blast_type: str = 'normal',
331                 source_player: ba.Player | None = None,
332                 hit_type: str = 'explosion',
333                 hit_subtype: str = 'normal'):
334        """Instantiate with given values."""
335
336        # bah; get off my lawn!
337        # pylint: disable=too-many-locals
338        # pylint: disable=too-many-statements
339
340        super().__init__()
341
342        shared = SharedObjects.get()
343        factory = BombFactory.get()
344
345        self.blast_type = blast_type
346        self._source_player = source_player
347        self.hit_type = hit_type
348        self.hit_subtype = hit_subtype
349        self.radius = blast_radius
350
351        # Set our position a bit lower so we throw more things upward.
352        rmats = (factory.blast_material, shared.attack_material)
353        self.node = ba.newnode(
354            'region',
355            delegate=self,
356            attrs={
357                'position': (position[0], position[1] - 0.1, position[2]),
358                'scale': (self.radius, self.radius, self.radius),
359                'type': 'sphere',
360                'materials': rmats
361            },
362        )
363
364        ba.timer(0.05, self.node.delete)
365
366        # Throw in an explosion and flash.
367        evel = (velocity[0], max(-1.0, velocity[1]), velocity[2])
368        explosion = ba.newnode('explosion',
369                               attrs={
370                                   'position': position,
371                                   'velocity': evel,
372                                   'radius': self.radius,
373                                   'big': (self.blast_type == 'tnt')
374                               })
375        if self.blast_type == 'ice':
376            explosion.color = (0, 0.05, 0.4)
377
378        ba.timer(1.0, explosion.delete)
379
380        if self.blast_type != 'ice':
381            ba.emitfx(position=position,
382                      velocity=velocity,
383                      count=int(1.0 + random.random() * 4),
384                      emit_type='tendrils',
385                      tendril_type='thin_smoke')
386        ba.emitfx(position=position,
387                  velocity=velocity,
388                  count=int(4.0 + random.random() * 4),
389                  emit_type='tendrils',
390                  tendril_type='ice' if self.blast_type == 'ice' else 'smoke')
391        ba.emitfx(position=position,
392                  emit_type='distortion',
393                  spread=1.0 if self.blast_type == 'tnt' else 2.0)
394
395        # And emit some shrapnel.
396        if self.blast_type == 'ice':
397
398            def emit() -> None:
399                ba.emitfx(position=position,
400                          velocity=velocity,
401                          count=30,
402                          spread=2.0,
403                          scale=0.4,
404                          chunk_type='ice',
405                          emit_type='stickers')
406
407            # It looks better if we delay a bit.
408            ba.timer(0.05, emit)
409
410        elif self.blast_type == 'sticky':
411
412            def emit() -> None:
413                ba.emitfx(position=position,
414                          velocity=velocity,
415                          count=int(4.0 + random.random() * 8),
416                          spread=0.7,
417                          chunk_type='slime')
418                ba.emitfx(position=position,
419                          velocity=velocity,
420                          count=int(4.0 + random.random() * 8),
421                          scale=0.5,
422                          spread=0.7,
423                          chunk_type='slime')
424                ba.emitfx(position=position,
425                          velocity=velocity,
426                          count=15,
427                          scale=0.6,
428                          chunk_type='slime',
429                          emit_type='stickers')
430                ba.emitfx(position=position,
431                          velocity=velocity,
432                          count=20,
433                          scale=0.7,
434                          chunk_type='spark',
435                          emit_type='stickers')
436                ba.emitfx(position=position,
437                          velocity=velocity,
438                          count=int(6.0 + random.random() * 12),
439                          scale=0.8,
440                          spread=1.5,
441                          chunk_type='spark')
442
443            # It looks better if we delay a bit.
444            ba.timer(0.05, emit)
445
446        elif self.blast_type == 'impact':
447
448            def emit() -> None:
449                ba.emitfx(position=position,
450                          velocity=velocity,
451                          count=int(4.0 + random.random() * 8),
452                          scale=0.8,
453                          chunk_type='metal')
454                ba.emitfx(position=position,
455                          velocity=velocity,
456                          count=int(4.0 + random.random() * 8),
457                          scale=0.4,
458                          chunk_type='metal')
459                ba.emitfx(position=position,
460                          velocity=velocity,
461                          count=20,
462                          scale=0.7,
463                          chunk_type='spark',
464                          emit_type='stickers')
465                ba.emitfx(position=position,
466                          velocity=velocity,
467                          count=int(8.0 + random.random() * 15),
468                          scale=0.8,
469                          spread=1.5,
470                          chunk_type='spark')
471
472            # It looks better if we delay a bit.
473            ba.timer(0.05, emit)
474
475        else:  # Regular or land mine bomb shrapnel.
476
477            def emit() -> None:
478                if self.blast_type != 'tnt':
479                    ba.emitfx(position=position,
480                              velocity=velocity,
481                              count=int(4.0 + random.random() * 8),
482                              chunk_type='rock')
483                    ba.emitfx(position=position,
484                              velocity=velocity,
485                              count=int(4.0 + random.random() * 8),
486                              scale=0.5,
487                              chunk_type='rock')
488                ba.emitfx(position=position,
489                          velocity=velocity,
490                          count=30,
491                          scale=1.0 if self.blast_type == 'tnt' else 0.7,
492                          chunk_type='spark',
493                          emit_type='stickers')
494                ba.emitfx(position=position,
495                          velocity=velocity,
496                          count=int(18.0 + random.random() * 20),
497                          scale=1.0 if self.blast_type == 'tnt' else 0.8,
498                          spread=1.5,
499                          chunk_type='spark')
500
501                # TNT throws splintery chunks.
502                if self.blast_type == 'tnt':
503
504                    def emit_splinters() -> None:
505                        ba.emitfx(position=position,
506                                  velocity=velocity,
507                                  count=int(20.0 + random.random() * 25),
508                                  scale=0.8,
509                                  spread=1.0,
510                                  chunk_type='splinter')
511
512                    ba.timer(0.01, emit_splinters)
513
514                # Every now and then do a sparky one.
515                if self.blast_type == 'tnt' or random.random() < 0.1:
516
517                    def emit_extra_sparks() -> None:
518                        ba.emitfx(position=position,
519                                  velocity=velocity,
520                                  count=int(10.0 + random.random() * 20),
521                                  scale=0.8,
522                                  spread=1.5,
523                                  chunk_type='spark')
524
525                    ba.timer(0.02, emit_extra_sparks)
526
527            # It looks better if we delay a bit.
528            ba.timer(0.05, emit)
529
530        lcolor = ((0.6, 0.6, 1.0) if self.blast_type == 'ice' else
531                  (1, 0.3, 0.1))
532        light = ba.newnode('light',
533                           attrs={
534                               'position': position,
535                               'volume_intensity_scale': 10.0,
536                               'color': lcolor
537                           })
538
539        scl = random.uniform(0.6, 0.9)
540        scorch_radius = light_radius = self.radius
541        if self.blast_type == 'tnt':
542            light_radius *= 1.4
543            scorch_radius *= 1.15
544            scl *= 3.0
545
546        iscale = 1.6
547        ba.animate(
548            light, 'intensity', {
549                0: 2.0 * iscale,
550                scl * 0.02: 0.1 * iscale,
551                scl * 0.025: 0.2 * iscale,
552                scl * 0.05: 17.0 * iscale,
553                scl * 0.06: 5.0 * iscale,
554                scl * 0.08: 4.0 * iscale,
555                scl * 0.2: 0.6 * iscale,
556                scl * 2.0: 0.00 * iscale,
557                scl * 3.0: 0.0
558            })
559        ba.animate(
560            light, 'radius', {
561                0: light_radius * 0.2,
562                scl * 0.05: light_radius * 0.55,
563                scl * 0.1: light_radius * 0.3,
564                scl * 0.3: light_radius * 0.15,
565                scl * 1.0: light_radius * 0.05
566            })
567        ba.timer(scl * 3.0, light.delete)
568
569        # Make a scorch that fades over time.
570        scorch = ba.newnode('scorch',
571                            attrs={
572                                'position': position,
573                                'size': scorch_radius * 0.5,
574                                'big': (self.blast_type == 'tnt')
575                            })
576        if self.blast_type == 'ice':
577            scorch.color = (1, 1, 1.5)
578
579        ba.animate(scorch, 'presence', {3.000: 1, 13.000: 0})
580        ba.timer(13.0, scorch.delete)
581
582        if self.blast_type == 'ice':
583            ba.playsound(factory.hiss_sound, position=light.position)
584
585        lpos = light.position
586        ba.playsound(factory.random_explode_sound(), position=lpos)
587        ba.playsound(factory.debris_fall_sound, position=lpos)
588
589        ba.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0)
590
591        # TNT is more epic.
592        if self.blast_type == 'tnt':
593            ba.playsound(factory.random_explode_sound(), position=lpos)
594
595            def _extra_boom() -> None:
596                ba.playsound(factory.random_explode_sound(), position=lpos)
597
598            ba.timer(0.25, _extra_boom)
599
600            def _extra_debris_sound() -> None:
601                ba.playsound(factory.debris_fall_sound, position=lpos)
602                ba.playsound(factory.wood_debris_fall_sound, position=lpos)
603
604            ba.timer(0.4, _extra_debris_sound)
605
606    def handlemessage(self, msg: Any) -> Any:
607        assert not self.expired
608
609        if isinstance(msg, ba.DieMessage):
610            if self.node:
611                self.node.delete()
612
613        elif isinstance(msg, ExplodeHitMessage):
614            node = ba.getcollision().opposingnode
615            assert self.node
616            nodepos = self.node.position
617            mag = 2000.0
618            if self.blast_type == 'ice':
619                mag *= 0.5
620            elif self.blast_type == 'land_mine':
621                mag *= 2.5
622            elif self.blast_type == 'tnt':
623                mag *= 2.0
624
625            node.handlemessage(
626                ba.HitMessage(pos=nodepos,
627                              velocity=(0, 0, 0),
628                              magnitude=mag,
629                              hit_type=self.hit_type,
630                              hit_subtype=self.hit_subtype,
631                              radius=self.radius,
632                              source_player=ba.existing(self._source_player)))
633            if self.blast_type == 'ice':
634                ba.playsound(BombFactory.get().freeze_sound,
635                             10,
636                             position=nodepos)
637                node.handlemessage(ba.FreezeMessage())
638
639        else:
640            return super().handlemessage(msg)
641        return None

An explosion, as generated by a bomb or some other object.

category: Gameplay Classes

Blast( 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: ba._player.Player | None = None, hit_type: str = 'explosion', hit_subtype: str = 'normal')
326    def __init__(self,
327                 position: Sequence[float] = (0.0, 1.0, 0.0),
328                 velocity: Sequence[float] = (0.0, 0.0, 0.0),
329                 blast_radius: float = 2.0,
330                 blast_type: str = 'normal',
331                 source_player: ba.Player | None = None,
332                 hit_type: str = 'explosion',
333                 hit_subtype: str = 'normal'):
334        """Instantiate with given values."""
335
336        # bah; get off my lawn!
337        # pylint: disable=too-many-locals
338        # pylint: disable=too-many-statements
339
340        super().__init__()
341
342        shared = SharedObjects.get()
343        factory = BombFactory.get()
344
345        self.blast_type = blast_type
346        self._source_player = source_player
347        self.hit_type = hit_type
348        self.hit_subtype = hit_subtype
349        self.radius = blast_radius
350
351        # Set our position a bit lower so we throw more things upward.
352        rmats = (factory.blast_material, shared.attack_material)
353        self.node = ba.newnode(
354            'region',
355            delegate=self,
356            attrs={
357                'position': (position[0], position[1] - 0.1, position[2]),
358                'scale': (self.radius, self.radius, self.radius),
359                'type': 'sphere',
360                'materials': rmats
361            },
362        )
363
364        ba.timer(0.05, self.node.delete)
365
366        # Throw in an explosion and flash.
367        evel = (velocity[0], max(-1.0, velocity[1]), velocity[2])
368        explosion = ba.newnode('explosion',
369                               attrs={
370                                   'position': position,
371                                   'velocity': evel,
372                                   'radius': self.radius,
373                                   'big': (self.blast_type == 'tnt')
374                               })
375        if self.blast_type == 'ice':
376            explosion.color = (0, 0.05, 0.4)
377
378        ba.timer(1.0, explosion.delete)
379
380        if self.blast_type != 'ice':
381            ba.emitfx(position=position,
382                      velocity=velocity,
383                      count=int(1.0 + random.random() * 4),
384                      emit_type='tendrils',
385                      tendril_type='thin_smoke')
386        ba.emitfx(position=position,
387                  velocity=velocity,
388                  count=int(4.0 + random.random() * 4),
389                  emit_type='tendrils',
390                  tendril_type='ice' if self.blast_type == 'ice' else 'smoke')
391        ba.emitfx(position=position,
392                  emit_type='distortion',
393                  spread=1.0 if self.blast_type == 'tnt' else 2.0)
394
395        # And emit some shrapnel.
396        if self.blast_type == 'ice':
397
398            def emit() -> None:
399                ba.emitfx(position=position,
400                          velocity=velocity,
401                          count=30,
402                          spread=2.0,
403                          scale=0.4,
404                          chunk_type='ice',
405                          emit_type='stickers')
406
407            # It looks better if we delay a bit.
408            ba.timer(0.05, emit)
409
410        elif self.blast_type == 'sticky':
411
412            def emit() -> None:
413                ba.emitfx(position=position,
414                          velocity=velocity,
415                          count=int(4.0 + random.random() * 8),
416                          spread=0.7,
417                          chunk_type='slime')
418                ba.emitfx(position=position,
419                          velocity=velocity,
420                          count=int(4.0 + random.random() * 8),
421                          scale=0.5,
422                          spread=0.7,
423                          chunk_type='slime')
424                ba.emitfx(position=position,
425                          velocity=velocity,
426                          count=15,
427                          scale=0.6,
428                          chunk_type='slime',
429                          emit_type='stickers')
430                ba.emitfx(position=position,
431                          velocity=velocity,
432                          count=20,
433                          scale=0.7,
434                          chunk_type='spark',
435                          emit_type='stickers')
436                ba.emitfx(position=position,
437                          velocity=velocity,
438                          count=int(6.0 + random.random() * 12),
439                          scale=0.8,
440                          spread=1.5,
441                          chunk_type='spark')
442
443            # It looks better if we delay a bit.
444            ba.timer(0.05, emit)
445
446        elif self.blast_type == 'impact':
447
448            def emit() -> None:
449                ba.emitfx(position=position,
450                          velocity=velocity,
451                          count=int(4.0 + random.random() * 8),
452                          scale=0.8,
453                          chunk_type='metal')
454                ba.emitfx(position=position,
455                          velocity=velocity,
456                          count=int(4.0 + random.random() * 8),
457                          scale=0.4,
458                          chunk_type='metal')
459                ba.emitfx(position=position,
460                          velocity=velocity,
461                          count=20,
462                          scale=0.7,
463                          chunk_type='spark',
464                          emit_type='stickers')
465                ba.emitfx(position=position,
466                          velocity=velocity,
467                          count=int(8.0 + random.random() * 15),
468                          scale=0.8,
469                          spread=1.5,
470                          chunk_type='spark')
471
472            # It looks better if we delay a bit.
473            ba.timer(0.05, emit)
474
475        else:  # Regular or land mine bomb shrapnel.
476
477            def emit() -> None:
478                if self.blast_type != 'tnt':
479                    ba.emitfx(position=position,
480                              velocity=velocity,
481                              count=int(4.0 + random.random() * 8),
482                              chunk_type='rock')
483                    ba.emitfx(position=position,
484                              velocity=velocity,
485                              count=int(4.0 + random.random() * 8),
486                              scale=0.5,
487                              chunk_type='rock')
488                ba.emitfx(position=position,
489                          velocity=velocity,
490                          count=30,
491                          scale=1.0 if self.blast_type == 'tnt' else 0.7,
492                          chunk_type='spark',
493                          emit_type='stickers')
494                ba.emitfx(position=position,
495                          velocity=velocity,
496                          count=int(18.0 + random.random() * 20),
497                          scale=1.0 if self.blast_type == 'tnt' else 0.8,
498                          spread=1.5,
499                          chunk_type='spark')
500
501                # TNT throws splintery chunks.
502                if self.blast_type == 'tnt':
503
504                    def emit_splinters() -> None:
505                        ba.emitfx(position=position,
506                                  velocity=velocity,
507                                  count=int(20.0 + random.random() * 25),
508                                  scale=0.8,
509                                  spread=1.0,
510                                  chunk_type='splinter')
511
512                    ba.timer(0.01, emit_splinters)
513
514                # Every now and then do a sparky one.
515                if self.blast_type == 'tnt' or random.random() < 0.1:
516
517                    def emit_extra_sparks() -> None:
518                        ba.emitfx(position=position,
519                                  velocity=velocity,
520                                  count=int(10.0 + random.random() * 20),
521                                  scale=0.8,
522                                  spread=1.5,
523                                  chunk_type='spark')
524
525                    ba.timer(0.02, emit_extra_sparks)
526
527            # It looks better if we delay a bit.
528            ba.timer(0.05, emit)
529
530        lcolor = ((0.6, 0.6, 1.0) if self.blast_type == 'ice' else
531                  (1, 0.3, 0.1))
532        light = ba.newnode('light',
533                           attrs={
534                               'position': position,
535                               'volume_intensity_scale': 10.0,
536                               'color': lcolor
537                           })
538
539        scl = random.uniform(0.6, 0.9)
540        scorch_radius = light_radius = self.radius
541        if self.blast_type == 'tnt':
542            light_radius *= 1.4
543            scorch_radius *= 1.15
544            scl *= 3.0
545
546        iscale = 1.6
547        ba.animate(
548            light, 'intensity', {
549                0: 2.0 * iscale,
550                scl * 0.02: 0.1 * iscale,
551                scl * 0.025: 0.2 * iscale,
552                scl * 0.05: 17.0 * iscale,
553                scl * 0.06: 5.0 * iscale,
554                scl * 0.08: 4.0 * iscale,
555                scl * 0.2: 0.6 * iscale,
556                scl * 2.0: 0.00 * iscale,
557                scl * 3.0: 0.0
558            })
559        ba.animate(
560            light, 'radius', {
561                0: light_radius * 0.2,
562                scl * 0.05: light_radius * 0.55,
563                scl * 0.1: light_radius * 0.3,
564                scl * 0.3: light_radius * 0.15,
565                scl * 1.0: light_radius * 0.05
566            })
567        ba.timer(scl * 3.0, light.delete)
568
569        # Make a scorch that fades over time.
570        scorch = ba.newnode('scorch',
571                            attrs={
572                                'position': position,
573                                'size': scorch_radius * 0.5,
574                                'big': (self.blast_type == 'tnt')
575                            })
576        if self.blast_type == 'ice':
577            scorch.color = (1, 1, 1.5)
578
579        ba.animate(scorch, 'presence', {3.000: 1, 13.000: 0})
580        ba.timer(13.0, scorch.delete)
581
582        if self.blast_type == 'ice':
583            ba.playsound(factory.hiss_sound, position=light.position)
584
585        lpos = light.position
586        ba.playsound(factory.random_explode_sound(), position=lpos)
587        ba.playsound(factory.debris_fall_sound, position=lpos)
588
589        ba.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0)
590
591        # TNT is more epic.
592        if self.blast_type == 'tnt':
593            ba.playsound(factory.random_explode_sound(), position=lpos)
594
595            def _extra_boom() -> None:
596                ba.playsound(factory.random_explode_sound(), position=lpos)
597
598            ba.timer(0.25, _extra_boom)
599
600            def _extra_debris_sound() -> None:
601                ba.playsound(factory.debris_fall_sound, position=lpos)
602                ba.playsound(factory.wood_debris_fall_sound, position=lpos)
603
604            ba.timer(0.4, _extra_debris_sound)

Instantiate with given values.

def handlemessage(self, msg: Any) -> Any:
606    def handlemessage(self, msg: Any) -> Any:
607        assert not self.expired
608
609        if isinstance(msg, ba.DieMessage):
610            if self.node:
611                self.node.delete()
612
613        elif isinstance(msg, ExplodeHitMessage):
614            node = ba.getcollision().opposingnode
615            assert self.node
616            nodepos = self.node.position
617            mag = 2000.0
618            if self.blast_type == 'ice':
619                mag *= 0.5
620            elif self.blast_type == 'land_mine':
621                mag *= 2.5
622            elif self.blast_type == 'tnt':
623                mag *= 2.0
624
625            node.handlemessage(
626                ba.HitMessage(pos=nodepos,
627                              velocity=(0, 0, 0),
628                              magnitude=mag,
629                              hit_type=self.hit_type,
630                              hit_subtype=self.hit_subtype,
631                              radius=self.radius,
632                              source_player=ba.existing(self._source_player)))
633            if self.blast_type == 'ice':
634                ba.playsound(BombFactory.get().freeze_sound,
635                             10,
636                             position=nodepos)
637                node.handlemessage(ba.FreezeMessage())
638
639        else:
640            return super().handlemessage(msg)
641        return None

General message handling; can be passed any message object.

Inherited Members
ba._actor.Actor
autoretain
on_expire
expired
exists
is_alive
activity
getactivity
class Bomb(ba._actor.Actor):
 644class Bomb(ba.Actor):
 645    """A standard bomb and its variants such as land-mines and tnt-boxes.
 646
 647    category: Gameplay Classes
 648    """
 649
 650    # Ew; should try to clean this up later.
 651    # pylint: disable=too-many-locals
 652    # pylint: disable=too-many-branches
 653    # pylint: disable=too-many-statements
 654
 655    def __init__(self,
 656                 position: Sequence[float] = (0.0, 1.0, 0.0),
 657                 velocity: Sequence[float] = (0.0, 0.0, 0.0),
 658                 bomb_type: str = 'normal',
 659                 blast_radius: float = 2.0,
 660                 bomb_scale: float = 1.0,
 661                 source_player: ba.Player | None = None,
 662                 owner: ba.Node | None = None):
 663        """Create a new Bomb.
 664
 665        bomb_type can be 'ice','impact','land_mine','normal','sticky', or
 666        'tnt'. Note that for impact or land_mine bombs you have to call arm()
 667        before they will go off.
 668        """
 669        super().__init__()
 670
 671        shared = SharedObjects.get()
 672        factory = BombFactory.get()
 673
 674        if bomb_type not in ('ice', 'impact', 'land_mine', 'normal', 'sticky',
 675                             'tnt'):
 676            raise ValueError('invalid bomb type: ' + bomb_type)
 677        self.bomb_type = bomb_type
 678
 679        self._exploded = False
 680        self.scale = bomb_scale
 681
 682        self.texture_sequence: ba.Node | None = None
 683
 684        if self.bomb_type == 'sticky':
 685            self._last_sticky_sound_time = 0.0
 686
 687        self.blast_radius = blast_radius
 688        if self.bomb_type == 'ice':
 689            self.blast_radius *= 1.2
 690        elif self.bomb_type == 'impact':
 691            self.blast_radius *= 0.7
 692        elif self.bomb_type == 'land_mine':
 693            self.blast_radius *= 0.7
 694        elif self.bomb_type == 'tnt':
 695            self.blast_radius *= 1.45
 696
 697        self._explode_callbacks: list[Callable[[Bomb, Blast], Any]] = []
 698
 699        # The player this came from.
 700        self._source_player = source_player
 701
 702        # By default our hit type/subtype is our own, but we pick up types of
 703        # whoever sets us off so we know what caused a chain reaction.
 704        # UPDATE (July 2020): not inheriting hit-types anymore; this causes
 705        # weird effects such as land-mines inheriting 'punch' hit types and
 706        # then not being able to destroy certain things they normally could,
 707        # etc. Inheriting owner/source-node from things that set us off
 708        # should be all we need I think...
 709        self.hit_type = 'explosion'
 710        self.hit_subtype = self.bomb_type
 711
 712        # The node this came from.
 713        # FIXME: can we unify this and source_player?
 714        self.owner = owner
 715
 716        # Adding footing-materials to things can screw up jumping and flying
 717        # since players carrying those things and thus touching footing
 718        # objects will think they're on solid ground.. perhaps we don't
 719        # wanna add this even in the tnt case?
 720        materials: tuple[ba.Material, ...]
 721        if self.bomb_type == 'tnt':
 722            materials = (factory.bomb_material, shared.footing_material,
 723                         shared.object_material)
 724        else:
 725            materials = (factory.bomb_material, shared.object_material)
 726
 727        if self.bomb_type == 'impact':
 728            materials = materials + (factory.impact_blast_material, )
 729        elif self.bomb_type == 'land_mine':
 730            materials = materials + (factory.land_mine_no_explode_material, )
 731
 732        if self.bomb_type == 'sticky':
 733            materials = materials + (factory.sticky_material, )
 734        else:
 735            materials = materials + (factory.normal_sound_material, )
 736
 737        if self.bomb_type == 'land_mine':
 738            fuse_time = None
 739            self.node = ba.newnode('prop',
 740                                   delegate=self,
 741                                   attrs={
 742                                       'position': position,
 743                                       'velocity': velocity,
 744                                       'model': factory.land_mine_model,
 745                                       'light_model': factory.land_mine_model,
 746                                       'body': 'landMine',
 747                                       'body_scale': self.scale,
 748                                       'shadow_size': 0.44,
 749                                       'color_texture': factory.land_mine_tex,
 750                                       'reflection': 'powerup',
 751                                       'reflection_scale': [1.0],
 752                                       'materials': materials
 753                                   })
 754
 755        elif self.bomb_type == 'tnt':
 756            fuse_time = None
 757            self.node = ba.newnode('prop',
 758                                   delegate=self,
 759                                   attrs={
 760                                       'position': position,
 761                                       'velocity': velocity,
 762                                       'model': factory.tnt_model,
 763                                       'light_model': factory.tnt_model,
 764                                       'body': 'crate',
 765                                       'body_scale': self.scale,
 766                                       'shadow_size': 0.5,
 767                                       'color_texture': factory.tnt_tex,
 768                                       'reflection': 'soft',
 769                                       'reflection_scale': [0.23],
 770                                       'materials': materials
 771                                   })
 772
 773        elif self.bomb_type == 'impact':
 774            fuse_time = 20.0
 775            self.node = ba.newnode('prop',
 776                                   delegate=self,
 777                                   attrs={
 778                                       'position': position,
 779                                       'velocity': velocity,
 780                                       'body': 'sphere',
 781                                       'body_scale': self.scale,
 782                                       'model': factory.impact_bomb_model,
 783                                       'shadow_size': 0.3,
 784                                       'color_texture': factory.impact_tex,
 785                                       'reflection': 'powerup',
 786                                       'reflection_scale': [1.5],
 787                                       'materials': materials
 788                                   })
 789            self.arm_timer = ba.Timer(
 790                0.2, ba.WeakCall(self.handlemessage, ArmMessage()))
 791            self.warn_timer = ba.Timer(
 792                fuse_time - 1.7, ba.WeakCall(self.handlemessage,
 793                                             WarnMessage()))
 794
 795        else:
 796            fuse_time = 3.0
 797            if self.bomb_type == 'sticky':
 798                sticky = True
 799                model = factory.sticky_bomb_model
 800                rtype = 'sharper'
 801                rscale = 1.8
 802            else:
 803                sticky = False
 804                model = factory.bomb_model
 805                rtype = 'sharper'
 806                rscale = 1.8
 807            if self.bomb_type == 'ice':
 808                tex = factory.ice_tex
 809            elif self.bomb_type == 'sticky':
 810                tex = factory.sticky_tex
 811            else:
 812                tex = factory.regular_tex
 813            self.node = ba.newnode('bomb',
 814                                   delegate=self,
 815                                   attrs={
 816                                       'position': position,
 817                                       'velocity': velocity,
 818                                       'model': model,
 819                                       'body_scale': self.scale,
 820                                       'shadow_size': 0.3,
 821                                       'color_texture': tex,
 822                                       'sticky': sticky,
 823                                       'owner': owner,
 824                                       'reflection': rtype,
 825                                       'reflection_scale': [rscale],
 826                                       'materials': materials
 827                                   })
 828
 829            sound = ba.newnode('sound',
 830                               owner=self.node,
 831                               attrs={
 832                                   'sound': factory.fuse_sound,
 833                                   'volume': 0.25
 834                               })
 835            self.node.connectattr('position', sound, 'position')
 836            ba.animate(self.node, 'fuse_length', {0.0: 1.0, fuse_time: 0.0})
 837
 838        # Light the fuse!!!
 839        if self.bomb_type not in ('land_mine', 'tnt'):
 840            assert fuse_time is not None
 841            ba.timer(fuse_time,
 842                     ba.WeakCall(self.handlemessage, ExplodeMessage()))
 843
 844        ba.animate(self.node, 'model_scale', {
 845            0: 0,
 846            0.2: 1.3 * self.scale,
 847            0.26: self.scale
 848        })
 849
 850    def get_source_player(self,
 851                          playertype: type[PlayerType]) -> PlayerType | None:
 852        """Return the source-player if one exists and is the provided type."""
 853        player: Any = self._source_player
 854        return (player if isinstance(player, playertype) and player.exists()
 855                else None)
 856
 857    def on_expire(self) -> None:
 858        super().on_expire()
 859
 860        # Release callbacks/refs so we don't wind up with dependency loops.
 861        self._explode_callbacks = []
 862
 863    def _handle_die(self) -> None:
 864        if self.node:
 865            self.node.delete()
 866
 867    def _handle_oob(self) -> None:
 868        self.handlemessage(ba.DieMessage())
 869
 870    def _handle_impact(self) -> None:
 871        node = ba.getcollision().opposingnode
 872
 873        # If we're an impact bomb and we came from this node, don't explode.
 874        # (otherwise we blow up on our own head when jumping).
 875        # Alternately if we're hitting another impact-bomb from the same
 876        # source, don't explode. (can cause accidental explosions if rapidly
 877        # throwing/etc.)
 878        node_delegate = node.getdelegate(object)
 879        if node:
 880            if (self.bomb_type == 'impact' and
 881                (node is self.owner or
 882                 (isinstance(node_delegate, Bomb) and node_delegate.bomb_type
 883                  == 'impact' and node_delegate.owner is self.owner))):
 884                return
 885            self.handlemessage(ExplodeMessage())
 886
 887    def _handle_dropped(self) -> None:
 888        if self.bomb_type == 'land_mine':
 889            self.arm_timer = ba.Timer(
 890                1.25, ba.WeakCall(self.handlemessage, ArmMessage()))
 891
 892        # Once we've thrown a sticky bomb we can stick to it.
 893        elif self.bomb_type == 'sticky':
 894
 895            def _setsticky(node: ba.Node) -> None:
 896                if node:
 897                    node.stick_to_owner = True
 898
 899            ba.timer(0.25, lambda: _setsticky(self.node))
 900
 901    def _handle_splat(self) -> None:
 902        node = ba.getcollision().opposingnode
 903        if (node is not self.owner
 904                and ba.time() - self._last_sticky_sound_time > 1.0):
 905            self._last_sticky_sound_time = ba.time()
 906            assert self.node
 907            ba.playsound(BombFactory.get().sticky_impact_sound,
 908                         2.0,
 909                         position=self.node.position)
 910
 911    def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None:
 912        """Add a call to be run when the bomb has exploded.
 913
 914        The bomb and the new blast object are passed as arguments.
 915        """
 916        self._explode_callbacks.append(call)
 917
 918    def explode(self) -> None:
 919        """Blows up the bomb if it has not yet done so."""
 920        if self._exploded:
 921            return
 922        self._exploded = True
 923        if self.node:
 924            blast = Blast(position=self.node.position,
 925                          velocity=self.node.velocity,
 926                          blast_radius=self.blast_radius,
 927                          blast_type=self.bomb_type,
 928                          source_player=ba.existing(self._source_player),
 929                          hit_type=self.hit_type,
 930                          hit_subtype=self.hit_subtype).autoretain()
 931            for callback in self._explode_callbacks:
 932                callback(self, blast)
 933
 934        # We blew up so we need to go away.
 935        # NOTE TO SELF: do we actually need this delay?
 936        ba.timer(0.001, ba.WeakCall(self.handlemessage, ba.DieMessage()))
 937
 938    def _handle_warn(self) -> None:
 939        if self.texture_sequence and self.node:
 940            self.texture_sequence.rate = 30
 941            ba.playsound(BombFactory.get().warn_sound,
 942                         0.5,
 943                         position=self.node.position)
 944
 945    def _add_material(self, material: ba.Material) -> None:
 946        if not self.node:
 947            return
 948        materials = self.node.materials
 949        if material not in materials:
 950            assert isinstance(materials, tuple)
 951            self.node.materials = materials + (material, )
 952
 953    def arm(self) -> None:
 954        """Arm the bomb (for land-mines and impact-bombs).
 955
 956        These types of bombs will not explode until they have been armed.
 957        """
 958        if not self.node:
 959            return
 960        factory = BombFactory.get()
 961        intex: Sequence[ba.Texture]
 962        if self.bomb_type == 'land_mine':
 963            intex = (factory.land_mine_lit_tex, factory.land_mine_tex)
 964            self.texture_sequence = ba.newnode('texture_sequence',
 965                                               owner=self.node,
 966                                               attrs={
 967                                                   'rate': 30,
 968                                                   'input_textures': intex
 969                                               })
 970            ba.timer(0.5, self.texture_sequence.delete)
 971
 972            # We now make it explodable.
 973            ba.timer(
 974                0.25,
 975                ba.WeakCall(self._add_material,
 976                            factory.land_mine_blast_material))
 977        elif self.bomb_type == 'impact':
 978            intex = (factory.impact_lit_tex, factory.impact_tex,
 979                     factory.impact_tex)
 980            self.texture_sequence = ba.newnode('texture_sequence',
 981                                               owner=self.node,
 982                                               attrs={
 983                                                   'rate': 100,
 984                                                   'input_textures': intex
 985                                               })
 986            ba.timer(
 987                0.25,
 988                ba.WeakCall(self._add_material,
 989                            factory.land_mine_blast_material))
 990        else:
 991            raise Exception('arm() should only be called '
 992                            'on land-mines or impact bombs')
 993        self.texture_sequence.connectattr('output_texture', self.node,
 994                                          'color_texture')
 995        ba.playsound(factory.activate_sound, 0.5, position=self.node.position)
 996
 997    def _handle_hit(self, msg: ba.HitMessage) -> None:
 998        ispunched = (msg.srcnode and msg.srcnode.getnodetype() == 'spaz')
 999
1000        # Normal bombs are triggered by non-punch impacts;
1001        # impact-bombs by all impacts.
1002        if (not self._exploded and
1003            (not ispunched or self.bomb_type in ['impact', 'land_mine'])):
1004
1005            # Also lets change the owner of the bomb to whoever is setting
1006            # us off. (this way points for big chain reactions go to the
1007            # person causing them).
1008            source_player = msg.get_source_player(ba.Player)
1009            if source_player is not None:
1010                self._source_player = source_player
1011
1012                # Also inherit the hit type (if a landmine sets off by a bomb,
1013                # the credit should go to the mine)
1014                # the exception is TNT.  TNT always gets credit.
1015                # UPDATE (July 2020): not doing this anymore. Causes too much
1016                # weird logic such as bombs acting like punches. Holler if
1017                # anything is noticeably broken due to this.
1018                # if self.bomb_type != 'tnt':
1019                #     self.hit_type = msg.hit_type
1020                #     self.hit_subtype = msg.hit_subtype
1021
1022            ba.timer(0.1 + random.random() * 0.1,
1023                     ba.WeakCall(self.handlemessage, ExplodeMessage()))
1024        assert self.node
1025        self.node.handlemessage('impulse', msg.pos[0], msg.pos[1], msg.pos[2],
1026                                msg.velocity[0], msg.velocity[1],
1027                                msg.velocity[2], msg.magnitude,
1028                                msg.velocity_magnitude, msg.radius, 0,
1029                                msg.velocity[0], msg.velocity[1],
1030                                msg.velocity[2])
1031
1032        if msg.srcnode:
1033            pass
1034
1035    def handlemessage(self, msg: Any) -> Any:
1036        if isinstance(msg, ExplodeMessage):
1037            self.explode()
1038        elif isinstance(msg, ImpactMessage):
1039            self._handle_impact()
1040        # Ok the logic below looks like it was backwards to me.
1041        # Disabling for now; can bring back if need be.
1042        # elif isinstance(msg, ba.PickedUpMessage):
1043        #     # Change our source to whoever just picked us up *only* if it
1044        #     # is None. This way we can get points for killing bots with their
1045        #     # own bombs. Hmm would there be a downside to this?
1046        #     if self._source_player is not None:
1047        #         self._source_player = msg.node.source_player
1048        elif isinstance(msg, SplatMessage):
1049            self._handle_splat()
1050        elif isinstance(msg, ba.DroppedMessage):
1051            self._handle_dropped()
1052        elif isinstance(msg, ba.HitMessage):
1053            self._handle_hit(msg)
1054        elif isinstance(msg, ba.DieMessage):
1055            self._handle_die()
1056        elif isinstance(msg, ba.OutOfBoundsMessage):
1057            self._handle_oob()
1058        elif isinstance(msg, ArmMessage):
1059            self.arm()
1060        elif isinstance(msg, WarnMessage):
1061            self._handle_warn()
1062        else:
1063            super().handlemessage(msg)

A standard bomb and its variants such as land-mines and tnt-boxes.

category: Gameplay Classes

Bomb( 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: ba._player.Player | None = None, owner: _ba.Node | None = None)
655    def __init__(self,
656                 position: Sequence[float] = (0.0, 1.0, 0.0),
657                 velocity: Sequence[float] = (0.0, 0.0, 0.0),
658                 bomb_type: str = 'normal',
659                 blast_radius: float = 2.0,
660                 bomb_scale: float = 1.0,
661                 source_player: ba.Player | None = None,
662                 owner: ba.Node | None = None):
663        """Create a new Bomb.
664
665        bomb_type can be 'ice','impact','land_mine','normal','sticky', or
666        'tnt'. Note that for impact or land_mine bombs you have to call arm()
667        before they will go off.
668        """
669        super().__init__()
670
671        shared = SharedObjects.get()
672        factory = BombFactory.get()
673
674        if bomb_type not in ('ice', 'impact', 'land_mine', 'normal', 'sticky',
675                             'tnt'):
676            raise ValueError('invalid bomb type: ' + bomb_type)
677        self.bomb_type = bomb_type
678
679        self._exploded = False
680        self.scale = bomb_scale
681
682        self.texture_sequence: ba.Node | None = None
683
684        if self.bomb_type == 'sticky':
685            self._last_sticky_sound_time = 0.0
686
687        self.blast_radius = blast_radius
688        if self.bomb_type == 'ice':
689            self.blast_radius *= 1.2
690        elif self.bomb_type == 'impact':
691            self.blast_radius *= 0.7
692        elif self.bomb_type == 'land_mine':
693            self.blast_radius *= 0.7
694        elif self.bomb_type == 'tnt':
695            self.blast_radius *= 1.45
696
697        self._explode_callbacks: list[Callable[[Bomb, Blast], Any]] = []
698
699        # The player this came from.
700        self._source_player = source_player
701
702        # By default our hit type/subtype is our own, but we pick up types of
703        # whoever sets us off so we know what caused a chain reaction.
704        # UPDATE (July 2020): not inheriting hit-types anymore; this causes
705        # weird effects such as land-mines inheriting 'punch' hit types and
706        # then not being able to destroy certain things they normally could,
707        # etc. Inheriting owner/source-node from things that set us off
708        # should be all we need I think...
709        self.hit_type = 'explosion'
710        self.hit_subtype = self.bomb_type
711
712        # The node this came from.
713        # FIXME: can we unify this and source_player?
714        self.owner = owner
715
716        # Adding footing-materials to things can screw up jumping and flying
717        # since players carrying those things and thus touching footing
718        # objects will think they're on solid ground.. perhaps we don't
719        # wanna add this even in the tnt case?
720        materials: tuple[ba.Material, ...]
721        if self.bomb_type == 'tnt':
722            materials = (factory.bomb_material, shared.footing_material,
723                         shared.object_material)
724        else:
725            materials = (factory.bomb_material, shared.object_material)
726
727        if self.bomb_type == 'impact':
728            materials = materials + (factory.impact_blast_material, )
729        elif self.bomb_type == 'land_mine':
730            materials = materials + (factory.land_mine_no_explode_material, )
731
732        if self.bomb_type == 'sticky':
733            materials = materials + (factory.sticky_material, )
734        else:
735            materials = materials + (factory.normal_sound_material, )
736
737        if self.bomb_type == 'land_mine':
738            fuse_time = None
739            self.node = ba.newnode('prop',
740                                   delegate=self,
741                                   attrs={
742                                       'position': position,
743                                       'velocity': velocity,
744                                       'model': factory.land_mine_model,
745                                       'light_model': factory.land_mine_model,
746                                       'body': 'landMine',
747                                       'body_scale': self.scale,
748                                       'shadow_size': 0.44,
749                                       'color_texture': factory.land_mine_tex,
750                                       'reflection': 'powerup',
751                                       'reflection_scale': [1.0],
752                                       'materials': materials
753                                   })
754
755        elif self.bomb_type == 'tnt':
756            fuse_time = None
757            self.node = ba.newnode('prop',
758                                   delegate=self,
759                                   attrs={
760                                       'position': position,
761                                       'velocity': velocity,
762                                       'model': factory.tnt_model,
763                                       'light_model': factory.tnt_model,
764                                       'body': 'crate',
765                                       'body_scale': self.scale,
766                                       'shadow_size': 0.5,
767                                       'color_texture': factory.tnt_tex,
768                                       'reflection': 'soft',
769                                       'reflection_scale': [0.23],
770                                       'materials': materials
771                                   })
772
773        elif self.bomb_type == 'impact':
774            fuse_time = 20.0
775            self.node = ba.newnode('prop',
776                                   delegate=self,
777                                   attrs={
778                                       'position': position,
779                                       'velocity': velocity,
780                                       'body': 'sphere',
781                                       'body_scale': self.scale,
782                                       'model': factory.impact_bomb_model,
783                                       'shadow_size': 0.3,
784                                       'color_texture': factory.impact_tex,
785                                       'reflection': 'powerup',
786                                       'reflection_scale': [1.5],
787                                       'materials': materials
788                                   })
789            self.arm_timer = ba.Timer(
790                0.2, ba.WeakCall(self.handlemessage, ArmMessage()))
791            self.warn_timer = ba.Timer(
792                fuse_time - 1.7, ba.WeakCall(self.handlemessage,
793                                             WarnMessage()))
794
795        else:
796            fuse_time = 3.0
797            if self.bomb_type == 'sticky':
798                sticky = True
799                model = factory.sticky_bomb_model
800                rtype = 'sharper'
801                rscale = 1.8
802            else:
803                sticky = False
804                model = factory.bomb_model
805                rtype = 'sharper'
806                rscale = 1.8
807            if self.bomb_type == 'ice':
808                tex = factory.ice_tex
809            elif self.bomb_type == 'sticky':
810                tex = factory.sticky_tex
811            else:
812                tex = factory.regular_tex
813            self.node = ba.newnode('bomb',
814                                   delegate=self,
815                                   attrs={
816                                       'position': position,
817                                       'velocity': velocity,
818                                       'model': model,
819                                       'body_scale': self.scale,
820                                       'shadow_size': 0.3,
821                                       'color_texture': tex,
822                                       'sticky': sticky,
823                                       'owner': owner,
824                                       'reflection': rtype,
825                                       'reflection_scale': [rscale],
826                                       'materials': materials
827                                   })
828
829            sound = ba.newnode('sound',
830                               owner=self.node,
831                               attrs={
832                                   'sound': factory.fuse_sound,
833                                   'volume': 0.25
834                               })
835            self.node.connectattr('position', sound, 'position')
836            ba.animate(self.node, 'fuse_length', {0.0: 1.0, fuse_time: 0.0})
837
838        # Light the fuse!!!
839        if self.bomb_type not in ('land_mine', 'tnt'):
840            assert fuse_time is not None
841            ba.timer(fuse_time,
842                     ba.WeakCall(self.handlemessage, ExplodeMessage()))
843
844        ba.animate(self.node, 'model_scale', {
845            0: 0,
846            0.2: 1.3 * self.scale,
847            0.26: self.scale
848        })

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.

def get_source_player(self, playertype: type[~PlayerType]) -> Optional[~PlayerType]:
850    def get_source_player(self,
851                          playertype: type[PlayerType]) -> PlayerType | None:
852        """Return the source-player if one exists and is the provided type."""
853        player: Any = self._source_player
854        return (player if isinstance(player, playertype) and player.exists()
855                else None)

Return the source-player if one exists and is the provided type.

def on_expire(self) -> None:
857    def on_expire(self) -> None:
858        super().on_expire()
859
860        # Release callbacks/refs so we don't wind up with dependency loops.
861        self._explode_callbacks = []

Called for remaining ba.Actors when their ba.Activity shuts down.

Actors can use this opportunity to clear callbacks or other references which have the potential of keeping the ba.Activity alive inadvertently (Activities can not exit cleanly while any Python references to them remain.)

Once an actor is expired (see ba.Actor.is_expired()) it should no longer perform any game-affecting operations (creating, modifying, or deleting nodes, media, timers, etc.) Attempts to do so will likely result in errors.

def add_explode_callback( self, call: Callable[[bastd.actor.bomb.Bomb, bastd.actor.bomb.Blast], Any]) -> None:
911    def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None:
912        """Add a call to be run when the bomb has exploded.
913
914        The bomb and the new blast object are passed as arguments.
915        """
916        self._explode_callbacks.append(call)

Add a call to be run when the bomb has exploded.

The bomb and the new blast object are passed as arguments.

def explode(self) -> None:
918    def explode(self) -> None:
919        """Blows up the bomb if it has not yet done so."""
920        if self._exploded:
921            return
922        self._exploded = True
923        if self.node:
924            blast = Blast(position=self.node.position,
925                          velocity=self.node.velocity,
926                          blast_radius=self.blast_radius,
927                          blast_type=self.bomb_type,
928                          source_player=ba.existing(self._source_player),
929                          hit_type=self.hit_type,
930                          hit_subtype=self.hit_subtype).autoretain()
931            for callback in self._explode_callbacks:
932                callback(self, blast)
933
934        # We blew up so we need to go away.
935        # NOTE TO SELF: do we actually need this delay?
936        ba.timer(0.001, ba.WeakCall(self.handlemessage, ba.DieMessage()))

Blows up the bomb if it has not yet done so.

def arm(self) -> None:
953    def arm(self) -> None:
954        """Arm the bomb (for land-mines and impact-bombs).
955
956        These types of bombs will not explode until they have been armed.
957        """
958        if not self.node:
959            return
960        factory = BombFactory.get()
961        intex: Sequence[ba.Texture]
962        if self.bomb_type == 'land_mine':
963            intex = (factory.land_mine_lit_tex, factory.land_mine_tex)
964            self.texture_sequence = ba.newnode('texture_sequence',
965                                               owner=self.node,
966                                               attrs={
967                                                   'rate': 30,
968                                                   'input_textures': intex
969                                               })
970            ba.timer(0.5, self.texture_sequence.delete)
971
972            # We now make it explodable.
973            ba.timer(
974                0.25,
975                ba.WeakCall(self._add_material,
976                            factory.land_mine_blast_material))
977        elif self.bomb_type == 'impact':
978            intex = (factory.impact_lit_tex, factory.impact_tex,
979                     factory.impact_tex)
980            self.texture_sequence = ba.newnode('texture_sequence',
981                                               owner=self.node,
982                                               attrs={
983                                                   'rate': 100,
984                                                   'input_textures': intex
985                                               })
986            ba.timer(
987                0.25,
988                ba.WeakCall(self._add_material,
989                            factory.land_mine_blast_material))
990        else:
991            raise Exception('arm() should only be called '
992                            'on land-mines or impact bombs')
993        self.texture_sequence.connectattr('output_texture', self.node,
994                                          'color_texture')
995        ba.playsound(factory.activate_sound, 0.5, position=self.node.position)

Arm the bomb (for land-mines and impact-bombs).

These types of bombs will not explode until they have been armed.

def handlemessage(self, msg: Any) -> Any:
1035    def handlemessage(self, msg: Any) -> Any:
1036        if isinstance(msg, ExplodeMessage):
1037            self.explode()
1038        elif isinstance(msg, ImpactMessage):
1039            self._handle_impact()
1040        # Ok the logic below looks like it was backwards to me.
1041        # Disabling for now; can bring back if need be.
1042        # elif isinstance(msg, ba.PickedUpMessage):
1043        #     # Change our source to whoever just picked us up *only* if it
1044        #     # is None. This way we can get points for killing bots with their
1045        #     # own bombs. Hmm would there be a downside to this?
1046        #     if self._source_player is not None:
1047        #         self._source_player = msg.node.source_player
1048        elif isinstance(msg, SplatMessage):
1049            self._handle_splat()
1050        elif isinstance(msg, ba.DroppedMessage):
1051            self._handle_dropped()
1052        elif isinstance(msg, ba.HitMessage):
1053            self._handle_hit(msg)
1054        elif isinstance(msg, ba.DieMessage):
1055            self._handle_die()
1056        elif isinstance(msg, ba.OutOfBoundsMessage):
1057            self._handle_oob()
1058        elif isinstance(msg, ArmMessage):
1059            self.arm()
1060        elif isinstance(msg, WarnMessage):
1061            self._handle_warn()
1062        else:
1063            super().handlemessage(msg)

General message handling; can be passed any message object.

Inherited Members
ba._actor.Actor
autoretain
expired
exists
is_alive
activity
getactivity
class TNTSpawner:
1066class TNTSpawner:
1067    """Regenerates TNT at a given point in space every now and then.
1068
1069    category: Gameplay Classes
1070    """
1071
1072    def __init__(self, position: Sequence[float], respawn_time: float = 20.0):
1073        """Instantiate with given position and respawn_time (in seconds)."""
1074        self._position = position
1075        self._tnt: Bomb | None = None
1076        self._respawn_time = random.uniform(0.8, 1.2) * respawn_time
1077        self._wait_time = 0.0
1078        self._update()
1079
1080        # Go with slightly more than 1 second to avoid timer stacking.
1081        self._update_timer = ba.Timer(1.1,
1082                                      ba.WeakCall(self._update),
1083                                      repeat=True)
1084
1085    def _update(self) -> None:
1086        tnt_alive = self._tnt is not None and self._tnt.node
1087        if not tnt_alive:
1088            # Respawn if its been long enough.. otherwise just increment our
1089            # how-long-since-we-died value.
1090            if self._tnt is None or self._wait_time >= self._respawn_time:
1091                self._tnt = Bomb(position=self._position, bomb_type='tnt')
1092                self._wait_time = 0.0
1093            else:
1094                self._wait_time += 1.1

Regenerates TNT at a given point in space every now and then.

category: Gameplay Classes

TNTSpawner(position: Sequence[float], respawn_time: float = 20.0)
1072    def __init__(self, position: Sequence[float], respawn_time: float = 20.0):
1073        """Instantiate with given position and respawn_time (in seconds)."""
1074        self._position = position
1075        self._tnt: Bomb | None = None
1076        self._respawn_time = random.uniform(0.8, 1.2) * respawn_time
1077        self._wait_time = 0.0
1078        self._update()
1079
1080        # Go with slightly more than 1 second to avoid timer stacking.
1081        self._update_timer = ba.Timer(1.1,
1082                                      ba.WeakCall(self._update),
1083                                      repeat=True)

Instantiate with given position and respawn_time (in seconds).