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

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 = (
177            ba.getsound('explosion01'),
178            ba.getsound('explosion02'),
179            ba.getsound('explosion03'),
180            ba.getsound('explosion04'),
181            ba.getsound('explosion05'),
182        )
183
184        self.freeze_sound = ba.getsound('freeze')
185        self.fuse_sound = ba.getsound('fuse01')
186        self.activate_sound = ba.getsound('activateBeep')
187        self.warn_sound = ba.getsound('warnBeep')
188
189        # Set up our material so new bombs don't collide with objects
190        # that they are initially overlapping.
191        self.bomb_material = ba.Material()
192        self.normal_sound_material = ba.Material()
193        self.sticky_material = ba.Material()
194
195        self.bomb_material.add_actions(
196            conditions=(
197                (
198                    ('we_are_younger_than', 100),
199                    'or',
200                    ('they_are_younger_than', 100),
201                ),
202                'and',
203                ('they_have_material', shared.object_material),
204            ),
205            actions=('modify_node_collision', 'collide', False),
206        )
207
208        # We want pickup materials to always hit us even if we're currently
209        # not colliding with their node. (generally due to the above rule)
210        self.bomb_material.add_actions(
211            conditions=('they_have_material', shared.pickup_material),
212            actions=('modify_part_collision', 'use_node_collide', False),
213        )
214
215        self.bomb_material.add_actions(
216            actions=('modify_part_collision', 'friction', 0.3)
217        )
218
219        self.land_mine_no_explode_material = ba.Material()
220        self.land_mine_blast_material = ba.Material()
221        self.land_mine_blast_material.add_actions(
222            conditions=(
223                ('we_are_older_than', 200),
224                'and',
225                ('they_are_older_than', 200),
226                'and',
227                ('eval_colliding',),
228                'and',
229                (
230                    (
231                        'they_dont_have_material',
232                        self.land_mine_no_explode_material,
233                    ),
234                    'and',
235                    (
236                        ('they_have_material', shared.object_material),
237                        'or',
238                        ('they_have_material', shared.player_material),
239                    ),
240                ),
241            ),
242            actions=('message', 'our_node', 'at_connect', ImpactMessage()),
243        )
244
245        self.impact_blast_material = ba.Material()
246        self.impact_blast_material.add_actions(
247            conditions=(
248                ('we_are_older_than', 200),
249                'and',
250                ('they_are_older_than', 200),
251                'and',
252                ('eval_colliding',),
253                'and',
254                (
255                    ('they_have_material', shared.footing_material),
256                    'or',
257                    ('they_have_material', shared.object_material),
258                ),
259            ),
260            actions=('message', 'our_node', 'at_connect', ImpactMessage()),
261        )
262
263        self.blast_material = ba.Material()
264        self.blast_material.add_actions(
265            conditions=('they_have_material', shared.object_material),
266            actions=(
267                ('modify_part_collision', 'collide', True),
268                ('modify_part_collision', 'physical', False),
269                ('message', 'our_node', 'at_connect', ExplodeHitMessage()),
270            ),
271        )
272
273        self.dink_sounds = (
274            ba.getsound('bombDrop01'),
275            ba.getsound('bombDrop02'),
276        )
277        self.sticky_impact_sound = ba.getsound('stickyImpact')
278        self.roll_sound = ba.getsound('bombRoll01')
279
280        # Collision sounds.
281        self.normal_sound_material.add_actions(
282            conditions=('they_have_material', shared.footing_material),
283            actions=(
284                ('impact_sound', self.dink_sounds, 2, 0.8),
285                ('roll_sound', self.roll_sound, 3, 6),
286            ),
287        )
288
289        self.sticky_material.add_actions(
290            actions=(
291                ('modify_part_collision', 'stiffness', 0.1),
292                ('modify_part_collision', 'damping', 1.0),
293            )
294        )
295
296        self.sticky_material.add_actions(
297            conditions=(
298                ('they_have_material', shared.player_material),
299                'or',
300                ('they_have_material', shared.footing_material),
301            ),
302            actions=('message', 'our_node', 'at_connect', SplatMessage()),
303        )

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:
306class SplatMessage:
307    """Tells an object to make a splat noise."""

Tells an object to make a splat noise.

SplatMessage()
class ExplodeMessage:
310class ExplodeMessage:
311    """Tells an object to explode."""

Tells an object to explode.

ExplodeMessage()
class ImpactMessage:
314class ImpactMessage:
315    """Tell an object it touched something."""

Tell an object it touched something.

ImpactMessage()
class ArmMessage:
318class ArmMessage:
319    """Tell an object to become armed."""

Tell an object to become armed.

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

Tell an object to issue a warning sound.

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

Tell an object it was hit by an explosion.

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

Instantiate with given values.

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

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]:
934    def get_source_player(
935        self, playertype: type[PlayerType]
936    ) -> PlayerType | None:
937        """Return the source-player if one exists and is the provided type."""
938        player: Any = self._source_player
939        return (
940            player
941            if isinstance(player, playertype) and player.exists()
942            else None
943        )

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

def on_expire(self) -> None:
945    def on_expire(self) -> None:
946        super().on_expire()
947
948        # Release callbacks/refs so we don't wind up with dependency loops.
949        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:
1008    def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None:
1009        """Add a call to be run when the bomb has exploded.
1010
1011        The bomb and the new blast object are passed as arguments.
1012        """
1013        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:
1015    def explode(self) -> None:
1016        """Blows up the bomb if it has not yet done so."""
1017        if self._exploded:
1018            return
1019        self._exploded = True
1020        if self.node:
1021            blast = Blast(
1022                position=self.node.position,
1023                velocity=self.node.velocity,
1024                blast_radius=self.blast_radius,
1025                blast_type=self.bomb_type,
1026                source_player=ba.existing(self._source_player),
1027                hit_type=self.hit_type,
1028                hit_subtype=self.hit_subtype,
1029            ).autoretain()
1030            for callback in self._explode_callbacks:
1031                callback(self, blast)
1032
1033        # We blew up so we need to go away.
1034        # NOTE TO SELF: do we actually need this delay?
1035        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:
1052    def arm(self) -> None:
1053        """Arm the bomb (for land-mines and impact-bombs).
1054
1055        These types of bombs will not explode until they have been armed.
1056        """
1057        if not self.node:
1058            return
1059        factory = BombFactory.get()
1060        intex: Sequence[ba.Texture]
1061        if self.bomb_type == 'land_mine':
1062            intex = (factory.land_mine_lit_tex, factory.land_mine_tex)
1063            self.texture_sequence = ba.newnode(
1064                'texture_sequence',
1065                owner=self.node,
1066                attrs={'rate': 30, 'input_textures': intex},
1067            )
1068            ba.timer(0.5, self.texture_sequence.delete)
1069
1070            # We now make it explodable.
1071            ba.timer(
1072                0.25,
1073                ba.WeakCall(
1074                    self._add_material, factory.land_mine_blast_material
1075                ),
1076            )
1077        elif self.bomb_type == 'impact':
1078            intex = (
1079                factory.impact_lit_tex,
1080                factory.impact_tex,
1081                factory.impact_tex,
1082            )
1083            self.texture_sequence = ba.newnode(
1084                'texture_sequence',
1085                owner=self.node,
1086                attrs={'rate': 100, 'input_textures': intex},
1087            )
1088            ba.timer(
1089                0.25,
1090                ba.WeakCall(
1091                    self._add_material, factory.land_mine_blast_material
1092                ),
1093            )
1094        else:
1095            raise RuntimeError(
1096                'arm() should only be called on land-mines or impact bombs'
1097            )
1098        self.texture_sequence.connectattr(
1099            'output_texture', self.node, 'color_texture'
1100        )
1101        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:
1154    def handlemessage(self, msg: Any) -> Any:
1155        if isinstance(msg, ExplodeMessage):
1156            self.explode()
1157        elif isinstance(msg, ImpactMessage):
1158            self._handle_impact()
1159        # Ok the logic below looks like it was backwards to me.
1160        # Disabling for now; can bring back if need be.
1161        # elif isinstance(msg, ba.PickedUpMessage):
1162        #     # Change our source to whoever just picked us up *only* if it
1163        #     # is None. This way we can get points for killing bots with their
1164        #     # own bombs. Hmm would there be a downside to this?
1165        #     if self._source_player is not None:
1166        #         self._source_player = msg.node.source_player
1167        elif isinstance(msg, SplatMessage):
1168            self._handle_splat()
1169        elif isinstance(msg, ba.DroppedMessage):
1170            self._handle_dropped()
1171        elif isinstance(msg, ba.HitMessage):
1172            self._handle_hit(msg)
1173        elif isinstance(msg, ba.DieMessage):
1174            self._handle_die()
1175        elif isinstance(msg, ba.OutOfBoundsMessage):
1176            self._handle_oob()
1177        elif isinstance(msg, ArmMessage):
1178            self.arm()
1179        elif isinstance(msg, WarnMessage):
1180            self._handle_warn()
1181        else:
1182            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:
1185class TNTSpawner:
1186    """Regenerates TNT at a given point in space every now and then.
1187
1188    category: Gameplay Classes
1189    """
1190
1191    def __init__(self, position: Sequence[float], respawn_time: float = 20.0):
1192        """Instantiate with given position and respawn_time (in seconds)."""
1193        self._position = position
1194        self._tnt: Bomb | None = None
1195        self._respawn_time = random.uniform(0.8, 1.2) * respawn_time
1196        self._wait_time = 0.0
1197        self._update()
1198
1199        # Go with slightly more than 1 second to avoid timer stacking.
1200        self._update_timer = ba.Timer(
1201            1.1, ba.WeakCall(self._update), repeat=True
1202        )
1203
1204    def _update(self) -> None:
1205        tnt_alive = self._tnt is not None and self._tnt.node
1206        if not tnt_alive:
1207            # Respawn if its been long enough.. otherwise just increment our
1208            # how-long-since-we-died value.
1209            if self._tnt is None or self._wait_time >= self._respawn_time:
1210                self._tnt = Bomb(position=self._position, bomb_type='tnt')
1211                self._wait_time = 0.0
1212            else:
1213                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)
1191    def __init__(self, position: Sequence[float], respawn_time: float = 20.0):
1192        """Instantiate with given position and respawn_time (in seconds)."""
1193        self._position = position
1194        self._tnt: Bomb | None = None
1195        self._respawn_time = random.uniform(0.8, 1.2) * respawn_time
1196        self._wait_time = 0.0
1197        self._update()
1198
1199        # Go with slightly more than 1 second to avoid timer stacking.
1200        self._update_timer = ba.Timer(
1201            1.1, ba.WeakCall(self._update), repeat=True
1202        )

Instantiate with given position and respawn_time (in seconds).