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

General message handling; can be passed any message object.

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

Create a new Bomb.

bomb_type can be 'ice','impact','land_mine','normal','sticky', or 'tnt'. Note that for impact or land_mine bombs you have to call arm() before they will go off.

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

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

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

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:
1006    def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None:
1007        """Add a call to be run when the bomb has exploded.
1008
1009        The bomb and the new blast object are passed as arguments.
1010        """
1011        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:
1013    def explode(self) -> None:
1014        """Blows up the bomb if it has not yet done so."""
1015        if self._exploded:
1016            return
1017        self._exploded = True
1018        if self.node:
1019            blast = Blast(
1020                position=self.node.position,
1021                velocity=self.node.velocity,
1022                blast_radius=self.blast_radius,
1023                blast_type=self.bomb_type,
1024                source_player=bs.existing(self._source_player),
1025                hit_type=self.hit_type,
1026                hit_subtype=self.hit_subtype,
1027            ).autoretain()
1028            for callback in self._explode_callbacks:
1029                callback(self, blast)
1030
1031        # We blew up so we need to go away.
1032        # NOTE TO SELF: do we actually need this delay?
1033        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:
1048    def arm(self) -> None:
1049        """Arm the bomb (for land-mines and impact-bombs).
1050
1051        These types of bombs will not explode until they have been armed.
1052        """
1053        if not self.node:
1054            return
1055        factory = BombFactory.get()
1056        intex: Sequence[bs.Texture]
1057        if self.bomb_type == 'land_mine':
1058            intex = (factory.land_mine_lit_tex, factory.land_mine_tex)
1059            self.texture_sequence = bs.newnode(
1060                'texture_sequence',
1061                owner=self.node,
1062                attrs={'rate': 30, 'input_textures': intex},
1063            )
1064            bs.timer(0.5, self.texture_sequence.delete)
1065
1066            # We now make it explodable.
1067            bs.timer(
1068                0.25,
1069                bs.WeakCall(
1070                    self._add_material, factory.land_mine_blast_material
1071                ),
1072            )
1073        elif self.bomb_type == 'impact':
1074            intex = (
1075                factory.impact_lit_tex,
1076                factory.impact_tex,
1077                factory.impact_tex,
1078            )
1079            self.texture_sequence = bs.newnode(
1080                'texture_sequence',
1081                owner=self.node,
1082                attrs={'rate': 100, 'input_textures': intex},
1083            )
1084            bs.timer(
1085                0.25,
1086                bs.WeakCall(
1087                    self._add_material, factory.land_mine_blast_material
1088                ),
1089            )
1090        else:
1091            raise RuntimeError(
1092                'arm() should only be called on land-mines or impact bombs'
1093            )
1094        self.texture_sequence.connectattr(
1095            'output_texture', self.node, 'color_texture'
1096        )
1097        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:
1149    @override
1150    def handlemessage(self, msg: Any) -> Any:
1151        if isinstance(msg, ExplodeMessage):
1152            self.explode()
1153        elif isinstance(msg, ImpactMessage):
1154            self._handle_impact()
1155        elif isinstance(msg, bs.PickedUpMessage):
1156            # Change our source to whoever just picked us up *only* if it
1157            # is None. This way we can get points for killing bots with their
1158            # own bombs. Hmm would there be a downside to this?
1159            if self._source_player is None:
1160                self._source_player = msg.node.source_player
1161        elif isinstance(msg, SplatMessage):
1162            self._handle_splat()
1163        elif isinstance(msg, bs.DroppedMessage):
1164            self._handle_dropped()
1165        elif isinstance(msg, bs.HitMessage):
1166            self._handle_hit(msg)
1167        elif isinstance(msg, bs.DieMessage):
1168            self._handle_die()
1169        elif isinstance(msg, bs.OutOfBoundsMessage):
1170            self._handle_oob()
1171        elif isinstance(msg, ArmMessage):
1172            self.arm()
1173        elif isinstance(msg, WarnMessage):
1174            self._handle_warn()
1175        else:
1176            super().handlemessage(msg)

General message handling; can be passed any message object.

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

Instantiate with given position and respawn_time (in seconds).