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

Wraps up media and other resources used by bs.Bombs.

Category: Gameplay Classes

A single instance of this is shared between all bombs and can be retrieved via bascenev1lib.actor.bomb.get_factory().

BombFactory()
148    def __init__(self) -> None:
149        """Instantiate a BombFactory.
150
151        You shouldn't need to do this; call
152        bascenev1lib.actor.bomb.get_factory() to get a shared instance.
153        """
154        shared = SharedObjects.get()
155
156        self.bomb_mesh = bs.getmesh('bomb')
157        self.sticky_bomb_mesh = bs.getmesh('bombSticky')
158        self.impact_bomb_mesh = bs.getmesh('impactBomb')
159        self.land_mine_mesh = bs.getmesh('landMine')
160        self.tnt_mesh = bs.getmesh('tnt')
161
162        self.regular_tex = bs.gettexture('bombColor')
163        self.ice_tex = bs.gettexture('bombColorIce')
164        self.sticky_tex = bs.gettexture('bombStickyColor')
165        self.impact_tex = bs.gettexture('impactBombColor')
166        self.impact_lit_tex = bs.gettexture('impactBombColorLit')
167        self.land_mine_tex = bs.gettexture('landMine')
168        self.land_mine_lit_tex = bs.gettexture('landMineLit')
169        self.tnt_tex = bs.gettexture('tnt')
170
171        self.hiss_sound = bs.getsound('hiss')
172        self.debris_fall_sound = bs.getsound('debrisFall')
173        self.wood_debris_fall_sound = bs.getsound('woodDebrisFall')
174
175        self.explode_sounds = (
176            bs.getsound('explosion01'),
177            bs.getsound('explosion02'),
178            bs.getsound('explosion03'),
179            bs.getsound('explosion04'),
180            bs.getsound('explosion05'),
181        )
182
183        self.freeze_sound = bs.getsound('freeze')
184        self.fuse_sound = bs.getsound('fuse01')
185        self.activate_sound = bs.getsound('activateBeep')
186        self.warn_sound = bs.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 = bs.Material()
191        self.normal_sound_material = bs.Material()
192        self.sticky_material = bs.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 = bs.Material()
219        self.land_mine_blast_material = bs.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 = bs.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 = bs.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            bs.getsound('bombDrop01'),
274            bs.getsound('bombDrop02'),
275        )
276        self.sticky_impact_sound = bs.getsound('stickyImpact')
277        self.roll_sound = bs.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        )

Instantiate a BombFactory.

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

bomb_mesh: _bascenev1.Mesh

The bs.Mesh of a standard or ice bomb.

sticky_bomb_mesh: _bascenev1.Mesh

The bs.Mesh of a sticky-bomb.

impact_bomb_mesh: _bascenev1.Mesh

The bs.Mesh of an impact-bomb.

land_mine_mesh: _bascenev1.Mesh

The bs.Mesh of a land-mine.

tnt_mesh: _bascenev1.Mesh

The bs.Mesh of a tnt box.

regular_tex: _bascenev1.Texture

The bs.Texture for regular bombs.

ice_tex: _bascenev1.Texture

The bs.Texture for ice bombs.

sticky_tex: _bascenev1.Texture

The bs.Texture for sticky bombs.

impact_tex: _bascenev1.Texture

The bs.Texture for impact bombs.

impact_lit_tex: _bascenev1.Texture

The bs.Texture for impact bombs with lights lit.

land_mine_tex: _bascenev1.Texture

The bs.Texture for land-mines.

land_mine_lit_tex: _bascenev1.Texture

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

tnt_tex: _bascenev1.Texture

The bs.Texture for tnt boxes.

hiss_sound: _bascenev1.Sound

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

debris_fall_sound: _bascenev1.Sound

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

wood_debris_fall_sound: _bascenev1.Sound

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

explode_sounds: Sequence[_bascenev1.Sound]

A tuple of bs.Sound-s for explosions.

freeze_sound: _bascenev1.Sound

A bs.Sound of an ice bomb freezing something.

fuse_sound: _bascenev1.Sound

A bs.Sound of a burning fuse.

activate_sound: _bascenev1.Sound

A bs.Sound for an activating impact bomb.

warn_sound: _bascenev1.Sound

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

bomb_material: _bascenev1.Material

A bs.Material applied to all bombs.

normal_sound_material: _bascenev1.Material

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

sticky_material: _bascenev1.Material

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

land_mine_no_explode_material: _bascenev1.Material

A bs.Material that keeps land-mines from blowing up. Applied to land-mines when they are created to allow land-mines to touch without exploding.

land_mine_blast_material: _bascenev1.Material

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

impact_blast_material: _bascenev1.Material

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

blast_material: _bascenev1.Material

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

dink_sounds: Sequence[_bascenev1.Sound]

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

sticky_impact_sound: _bascenev1.Sound

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

roll_sound: _bascenev1.Sound

bs.Sound for a rolling bomb.

@classmethod
def get(cls) -> BombFactory:
133    @classmethod
134    def get(cls) -> BombFactory:
135        """Get/create a shared bascenev1lib.actor.bomb.BombFactory object."""
136        activity = bs.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

Get/create a shared BombFactory object.

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

Return a random explosion bs.Sound from the factory.

class SplatMessage:
305class SplatMessage:
306    """Tells an object to make a splat noise."""

Tells an object to make a splat noise.

class ExplodeMessage:
309class ExplodeMessage:
310    """Tells an object to explode."""

Tells an object to explode.

class ImpactMessage:
313class ImpactMessage:
314    """Tell an object it touched something."""

Tell an object it touched something.

class ArmMessage:
317class ArmMessage:
318    """Tell an object to become armed."""

Tell an object to become armed.

class WarnMessage:
321class WarnMessage:
322    """Tell an object to issue a warning sound."""

Tell an object to issue a warning sound.

class ExplodeHitMessage:
325class ExplodeHitMessage:
326    """Tell an object it was hit by an explosion."""

Tell an object it was hit by an explosion.

class Blast(bascenev1._actor.Actor):
329class Blast(bs.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: bs.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 = bs.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        bs.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 = bs.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        bs.timer(1.0, explosion.delete)
392
393        if self.blast_type != 'ice':
394            bs.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        bs.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        bs.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                bs.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            bs.timer(0.05, emit)
430
431        elif self.blast_type == 'sticky':
432
433            def emit() -> None:
434                bs.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                bs.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                bs.emitfx(
450                    position=position,
451                    velocity=velocity,
452                    count=15,
453                    scale=0.6,
454                    chunk_type='slime',
455                    emit_type='stickers',
456                )
457                bs.emitfx(
458                    position=position,
459                    velocity=velocity,
460                    count=20,
461                    scale=0.7,
462                    chunk_type='spark',
463                    emit_type='stickers',
464                )
465                bs.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            bs.timer(0.05, emit)
476
477        elif self.blast_type == 'impact':
478
479            def emit() -> None:
480                bs.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                bs.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                bs.emitfx(
495                    position=position,
496                    velocity=velocity,
497                    count=20,
498                    scale=0.7,
499                    chunk_type='spark',
500                    emit_type='stickers',
501                )
502                bs.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            bs.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                    bs.emitfx(
519                        position=position,
520                        velocity=velocity,
521                        count=int(4.0 + random.random() * 8),
522                        chunk_type='rock',
523                    )
524                    bs.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                bs.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                bs.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                        bs.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                    bs.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                        bs.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                    bs.timer(0.02, emit_extra_sparks)
577
578            # It looks better if we delay a bit.
579            bs.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 = bs.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        bs.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        bs.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        bs.timer(scl * 3.0, light.delete)
626
627        # Make a scorch that fades over time.
628        scorch = bs.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        bs.animate(scorch, 'presence', {3.000: 1, 13.000: 0})
640        bs.timer(13.0, scorch.delete)
641
642        if self.blast_type == 'ice':
643            factory.hiss_sound.play(position=light.position)
644
645        lpos = light.position
646        factory.random_explode_sound().play(position=lpos)
647        factory.debris_fall_sound.play(position=lpos)
648
649        bs.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            factory.random_explode_sound().play(position=lpos)
654
655            def _extra_boom() -> None:
656                factory.random_explode_sound().play(position=lpos)
657
658            bs.timer(0.25, _extra_boom)
659
660            def _extra_debris_sound() -> None:
661                factory.debris_fall_sound.play(position=lpos)
662                factory.wood_debris_fall_sound.play(position=lpos)
663
664            bs.timer(0.4, _extra_debris_sound)
665
666    @override
667    def handlemessage(self, msg: Any) -> Any:
668        assert not self.expired
669
670        if isinstance(msg, bs.DieMessage):
671            if self.node:
672                self.node.delete()
673
674        elif isinstance(msg, ExplodeHitMessage):
675            node = bs.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                bs.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=bs.existing(self._source_player),
695                )
696            )
697            if self.blast_type == 'ice':
698                BombFactory.get().freeze_sound.play(10, position=nodepos)
699                node.handlemessage(bs.FreezeMessage())
700
701        else:
702            return super().handlemessage(msg)
703        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: bascenev1._player.Player | None = None, hit_type: str = 'explosion', hit_subtype: str = 'normal')
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: bs.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 = bs.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        bs.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 = bs.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        bs.timer(1.0, explosion.delete)
392
393        if self.blast_type != 'ice':
394            bs.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        bs.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        bs.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                bs.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            bs.timer(0.05, emit)
430
431        elif self.blast_type == 'sticky':
432
433            def emit() -> None:
434                bs.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                bs.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                bs.emitfx(
450                    position=position,
451                    velocity=velocity,
452                    count=15,
453                    scale=0.6,
454                    chunk_type='slime',
455                    emit_type='stickers',
456                )
457                bs.emitfx(
458                    position=position,
459                    velocity=velocity,
460                    count=20,
461                    scale=0.7,
462                    chunk_type='spark',
463                    emit_type='stickers',
464                )
465                bs.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            bs.timer(0.05, emit)
476
477        elif self.blast_type == 'impact':
478
479            def emit() -> None:
480                bs.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                bs.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                bs.emitfx(
495                    position=position,
496                    velocity=velocity,
497                    count=20,
498                    scale=0.7,
499                    chunk_type='spark',
500                    emit_type='stickers',
501                )
502                bs.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            bs.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                    bs.emitfx(
519                        position=position,
520                        velocity=velocity,
521                        count=int(4.0 + random.random() * 8),
522                        chunk_type='rock',
523                    )
524                    bs.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                bs.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                bs.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                        bs.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                    bs.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                        bs.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                    bs.timer(0.02, emit_extra_sparks)
577
578            # It looks better if we delay a bit.
579            bs.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 = bs.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        bs.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        bs.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        bs.timer(scl * 3.0, light.delete)
626
627        # Make a scorch that fades over time.
628        scorch = bs.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        bs.animate(scorch, 'presence', {3.000: 1, 13.000: 0})
640        bs.timer(13.0, scorch.delete)
641
642        if self.blast_type == 'ice':
643            factory.hiss_sound.play(position=light.position)
644
645        lpos = light.position
646        factory.random_explode_sound().play(position=lpos)
647        factory.debris_fall_sound.play(position=lpos)
648
649        bs.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            factory.random_explode_sound().play(position=lpos)
654
655            def _extra_boom() -> None:
656                factory.random_explode_sound().play(position=lpos)
657
658            bs.timer(0.25, _extra_boom)
659
660            def _extra_debris_sound() -> None:
661                factory.debris_fall_sound.play(position=lpos)
662                factory.wood_debris_fall_sound.play(position=lpos)
663
664            bs.timer(0.4, _extra_debris_sound)

Instantiate with given values.

blast_type
hit_type
hit_subtype
radius
node
@override
def handlemessage(self, msg: Any) -> Any:
666    @override
667    def handlemessage(self, msg: Any) -> Any:
668        assert not self.expired
669
670        if isinstance(msg, bs.DieMessage):
671            if self.node:
672                self.node.delete()
673
674        elif isinstance(msg, ExplodeHitMessage):
675            node = bs.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                bs.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=bs.existing(self._source_player),
695                )
696            )
697            if self.blast_type == 'ice':
698                BombFactory.get().freeze_sound.play(10, position=nodepos)
699                node.handlemessage(bs.FreezeMessage())
700
701        else:
702            return super().handlemessage(msg)
703        return None

General message handling; can be passed any message object.

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

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.

bomb_type
scale
texture_sequence: _bascenev1.Node | None
blast_radius
hit_type
hit_subtype
owner
def get_source_player(self, playertype: type[~PlayerT]) -> Optional[~PlayerT]:
932    def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None:
933        """Return the source-player if one exists and is the provided type."""
934        player: Any = self._source_player
935        return (
936            player
937            if isinstance(player, playertype) and player.exists()
938            else None
939        )

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

@override
def on_expire(self) -> None:
941    @override
942    def on_expire(self) -> None:
943        super().on_expire()
944
945        # Release callbacks/refs so we don't wind up with dependency loops.
946        self._explode_callbacks = []

Called for remaining bascenev1.Actors when their activity dies.

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

Once an actor is expired (see bascenev1.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[[Bomb, Blast], Any]) -> None:
1004    def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None:
1005        """Add a call to be run when the bomb has exploded.
1006
1007        The bomb and the new blast object are passed as arguments.
1008        """
1009        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:
1011    def explode(self) -> None:
1012        """Blows up the bomb if it has not yet done so."""
1013        if self._exploded:
1014            return
1015        self._exploded = True
1016        if self.node:
1017            blast = Blast(
1018                position=self.node.position,
1019                velocity=self.node.velocity,
1020                blast_radius=self.blast_radius,
1021                blast_type=self.bomb_type,
1022                source_player=bs.existing(self._source_player),
1023                hit_type=self.hit_type,
1024                hit_subtype=self.hit_subtype,
1025            ).autoretain()
1026            for callback in self._explode_callbacks:
1027                callback(self, blast)
1028
1029        # We blew up so we need to go away.
1030        # NOTE TO SELF: do we actually need this delay?
1031        bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage()))

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

def arm(self) -> None:
1046    def arm(self) -> None:
1047        """Arm the bomb (for land-mines and impact-bombs).
1048
1049        These types of bombs will not explode until they have been armed.
1050        """
1051        if not self.node:
1052            return
1053        factory = BombFactory.get()
1054        intex: Sequence[bs.Texture]
1055        if self.bomb_type == 'land_mine':
1056            intex = (factory.land_mine_lit_tex, factory.land_mine_tex)
1057            self.texture_sequence = bs.newnode(
1058                'texture_sequence',
1059                owner=self.node,
1060                attrs={'rate': 30, 'input_textures': intex},
1061            )
1062            bs.timer(0.5, self.texture_sequence.delete)
1063
1064            # We now make it explodable.
1065            bs.timer(
1066                0.25,
1067                bs.WeakCall(
1068                    self._add_material, factory.land_mine_blast_material
1069                ),
1070            )
1071        elif self.bomb_type == 'impact':
1072            intex = (
1073                factory.impact_lit_tex,
1074                factory.impact_tex,
1075                factory.impact_tex,
1076            )
1077            self.texture_sequence = bs.newnode(
1078                'texture_sequence',
1079                owner=self.node,
1080                attrs={'rate': 100, 'input_textures': intex},
1081            )
1082            bs.timer(
1083                0.25,
1084                bs.WeakCall(
1085                    self._add_material, factory.land_mine_blast_material
1086                ),
1087            )
1088        else:
1089            raise RuntimeError(
1090                'arm() should only be called on land-mines or impact bombs'
1091            )
1092        self.texture_sequence.connectattr(
1093            'output_texture', self.node, 'color_texture'
1094        )
1095        factory.activate_sound.play(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.

@override
def handlemessage(self, msg: Any) -> Any:
1147    @override
1148    def handlemessage(self, msg: Any) -> Any:
1149        if isinstance(msg, ExplodeMessage):
1150            self.explode()
1151        elif isinstance(msg, ImpactMessage):
1152            self._handle_impact()
1153        elif isinstance(msg, bs.PickedUpMessage):
1154            # Change our source to whoever just picked us up *only* if it
1155            # is None. This way we can get points for killing bots with their
1156            # own bombs. Hmm would there be a downside to this?
1157            if self._source_player is None:
1158                self._source_player = msg.node.source_player
1159        elif isinstance(msg, SplatMessage):
1160            self._handle_splat()
1161        elif isinstance(msg, bs.DroppedMessage):
1162            self._handle_dropped()
1163        elif isinstance(msg, bs.HitMessage):
1164            self._handle_hit(msg)
1165        elif isinstance(msg, bs.DieMessage):
1166            self._handle_die()
1167        elif isinstance(msg, bs.OutOfBoundsMessage):
1168            self._handle_oob()
1169        elif isinstance(msg, ArmMessage):
1170            self.arm()
1171        elif isinstance(msg, WarnMessage):
1172            self._handle_warn()
1173        else:
1174            super().handlemessage(msg)

General message handling; can be passed any message object.

Inherited Members
bascenev1._actor.Actor
autoretain
expired
exists
is_alive
activity
getactivity
class TNTSpawner:
1177class TNTSpawner:
1178    """Regenerates TNT at a given point in space every now and then.
1179
1180    category: Gameplay Classes
1181    """
1182
1183    def __init__(self, position: Sequence[float], respawn_time: float = 20.0):
1184        """Instantiate with given position and respawn_time (in seconds)."""
1185        self._position = position
1186        self._tnt: Bomb | None = None
1187        self._respawn_time = random.uniform(0.8, 1.2) * respawn_time
1188        self._wait_time = 0.0
1189        self._update()
1190
1191        # Go with slightly more than 1 second to avoid timer stacking.
1192        self._update_timer = bs.Timer(
1193            1.1, bs.WeakCall(self._update), repeat=True
1194        )
1195
1196    def _update(self) -> None:
1197        tnt_alive = self._tnt is not None and self._tnt.node
1198        if not tnt_alive:
1199            # Respawn if its been long enough.. otherwise just increment our
1200            # how-long-since-we-died value.
1201            if self._tnt is None or self._wait_time >= self._respawn_time:
1202                self._tnt = Bomb(position=self._position, bomb_type='tnt')
1203                self._wait_time = 0.0
1204            else:
1205                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)
1183    def __init__(self, position: Sequence[float], respawn_time: float = 20.0):
1184        """Instantiate with given position and respawn_time (in seconds)."""
1185        self._position = position
1186        self._tnt: Bomb | None = None
1187        self._respawn_time = random.uniform(0.8, 1.2) * respawn_time
1188        self._wait_time = 0.0
1189        self._update()
1190
1191        # Go with slightly more than 1 second to avoid timer stacking.
1192        self._update_timer = bs.Timer(
1193            1.1, bs.WeakCall(self._update), repeat=True
1194        )

Instantiate with given position and respawn_time (in seconds).