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

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

Instantiate a BombFactory.

You shouldn't need to do this; call 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:
134    @classmethod
135    def get(cls) -> BombFactory:
136        """Get/create a shared bascenev1lib.actor.bomb.BombFactory object."""
137        activity = bs.getactivity()
138        factory = activity.customdata.get(cls._STORENAME)
139        if factory is None:
140            factory = BombFactory()
141            activity.customdata[cls._STORENAME] = factory
142        assert isinstance(factory, BombFactory)
143        return factory

Get/create a shared BombFactory object.

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

Return a random explosion bs.Sound from the factory.

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

Tells an object to make a splat noise.

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

Tells an object to explode.

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

Tell an object it touched something.

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

Tell an object to become armed.

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

Tell an object to issue a warning sound.

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

Tell an object it was hit by an explosion.

class Blast(bascenev1._actor.Actor):
330class Blast(bs.Actor):
331    """An explosion, as generated by a bomb or some other object.
332
333    category: Gameplay Classes
334    """
335
336    def __init__(
337        self,
338        position: Sequence[float] = (0.0, 1.0, 0.0),
339        velocity: Sequence[float] = (0.0, 0.0, 0.0),
340        blast_radius: float = 2.0,
341        blast_type: str = 'normal',
342        source_player: 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.Player | None = None, hit_type: str = 'explosion', hit_subtype: str = 'normal')
336    def __init__(
337        self,
338        position: Sequence[float] = (0.0, 1.0, 0.0),
339        velocity: Sequence[float] = (0.0, 0.0, 0.0),
340        blast_radius: float = 2.0,
341        blast_type: str = 'normal',
342        source_player: 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.

Inherited Members
bascenev1._actor.Actor
autoretain
on_expire
expired
exists
is_alive
activity
getactivity
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        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)

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

category: Gameplay Classes

Bomb( position: Sequence[float] = (0.0, 1.0, 0.0), velocity: Sequence[float] = (0.0, 0.0, 0.0), bomb_type: str = 'normal', blast_radius: float = 2.0, bomb_scale: float = 1.0, source_player: bascenev1._player.Player | None = None, owner: _bascenev1.Node | None = None)
718    def __init__(
719        self,
720        position: Sequence[float] = (0.0, 1.0, 0.0),
721        velocity: Sequence[float] = (0.0, 0.0, 0.0),
722        bomb_type: str = 'normal',
723        blast_radius: float = 2.0,
724        bomb_scale: float = 1.0,
725        source_player: 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        )

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]:
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        )

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

@override
def on_expire(self) -> None:
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 = []

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:
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)

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

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

def arm(self) -> None:
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)

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:
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)

General message handling; can be passed any message object.

Inherited Members
bascenev1._actor.Actor
autoretain
expired
exists
is_alive
activity
getactivity
class TNTSpawner:
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

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

category: Gameplay Classes

TNTSpawner(position: Sequence[float], respawn_time: float = 20.0)
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        )

Instantiate with given position and respawn_time (in seconds).