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

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

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:
132    @classmethod
133    def get(cls) -> BombFactory:
134        """Get/create a shared bascenev1lib.actor.bomb.BombFactory object."""
135        activity = bs.getactivity()
136        factory = activity.customdata.get(cls._STORENAME)
137        if factory is None:
138            factory = BombFactory()
139            activity.customdata[cls._STORENAME] = factory
140        assert isinstance(factory, BombFactory)
141        return factory

Get/create a shared BombFactory object.

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

Return a random explosion bs.Sound from the factory.

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

Tells an object to make a splat noise.

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

Tells an object to explode.

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

Tell an object it touched something.

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

Tell an object to become armed.

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

Tell an object to issue a warning sound.

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

Tell an object it was hit by an explosion.

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

Instantiate with given values.

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

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

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

def on_expire(self) -> None:
939    def on_expire(self) -> None:
940        super().on_expire()
941
942        # Release callbacks/refs so we don't wind up with dependency loops.
943        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:
1001    def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None:
1002        """Add a call to be run when the bomb has exploded.
1003
1004        The bomb and the new blast object are passed as arguments.
1005        """
1006        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:
1008    def explode(self) -> None:
1009        """Blows up the bomb if it has not yet done so."""
1010        if self._exploded:
1011            return
1012        self._exploded = True
1013        if self.node:
1014            blast = Blast(
1015                position=self.node.position,
1016                velocity=self.node.velocity,
1017                blast_radius=self.blast_radius,
1018                blast_type=self.bomb_type,
1019                source_player=bs.existing(self._source_player),
1020                hit_type=self.hit_type,
1021                hit_subtype=self.hit_subtype,
1022            ).autoretain()
1023            for callback in self._explode_callbacks:
1024                callback(self, blast)
1025
1026        # We blew up so we need to go away.
1027        # NOTE TO SELF: do we actually need this delay?
1028        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:
1043    def arm(self) -> None:
1044        """Arm the bomb (for land-mines and impact-bombs).
1045
1046        These types of bombs will not explode until they have been armed.
1047        """
1048        if not self.node:
1049            return
1050        factory = BombFactory.get()
1051        intex: Sequence[bs.Texture]
1052        if self.bomb_type == 'land_mine':
1053            intex = (factory.land_mine_lit_tex, factory.land_mine_tex)
1054            self.texture_sequence = bs.newnode(
1055                'texture_sequence',
1056                owner=self.node,
1057                attrs={'rate': 30, 'input_textures': intex},
1058            )
1059            bs.timer(0.5, self.texture_sequence.delete)
1060
1061            # We now make it explodable.
1062            bs.timer(
1063                0.25,
1064                bs.WeakCall(
1065                    self._add_material, factory.land_mine_blast_material
1066                ),
1067            )
1068        elif self.bomb_type == 'impact':
1069            intex = (
1070                factory.impact_lit_tex,
1071                factory.impact_tex,
1072                factory.impact_tex,
1073            )
1074            self.texture_sequence = bs.newnode(
1075                'texture_sequence',
1076                owner=self.node,
1077                attrs={'rate': 100, 'input_textures': intex},
1078            )
1079            bs.timer(
1080                0.25,
1081                bs.WeakCall(
1082                    self._add_material, factory.land_mine_blast_material
1083                ),
1084            )
1085        else:
1086            raise RuntimeError(
1087                'arm() should only be called on land-mines or impact bombs'
1088            )
1089        self.texture_sequence.connectattr(
1090            'output_texture', self.node, 'color_texture'
1091        )
1092        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.

def handlemessage(self, msg: Any) -> Any:
1144    def handlemessage(self, msg: Any) -> Any:
1145        if isinstance(msg, ExplodeMessage):
1146            self.explode()
1147        elif isinstance(msg, ImpactMessage):
1148            self._handle_impact()
1149        # Ok the logic below looks like it was backwards to me.
1150        # Disabling for now; can bring back if need be.
1151        # elif isinstance(msg, bs.PickedUpMessage):
1152        #     # Change our source to whoever just picked us up *only* if it
1153        #     # is None. This way we can get points for killing bots with their
1154        #     # own bombs. Hmm would there be a downside to this?
1155        #     if self._source_player is not None:
1156        #         self._source_player = msg.node.source_player
1157        elif isinstance(msg, SplatMessage):
1158            self._handle_splat()
1159        elif isinstance(msg, bs.DroppedMessage):
1160            self._handle_dropped()
1161        elif isinstance(msg, bs.HitMessage):
1162            self._handle_hit(msg)
1163        elif isinstance(msg, bs.DieMessage):
1164            self._handle_die()
1165        elif isinstance(msg, bs.OutOfBoundsMessage):
1166            self._handle_oob()
1167        elif isinstance(msg, ArmMessage):
1168            self.arm()
1169        elif isinstance(msg, WarnMessage):
1170            self._handle_warn()
1171        else:
1172            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:
1175class TNTSpawner:
1176    """Regenerates TNT at a given point in space every now and then.
1177
1178    category: Gameplay Classes
1179    """
1180
1181    def __init__(self, position: Sequence[float], respawn_time: float = 20.0):
1182        """Instantiate with given position and respawn_time (in seconds)."""
1183        self._position = position
1184        self._tnt: Bomb | None = None
1185        self._respawn_time = random.uniform(0.8, 1.2) * respawn_time
1186        self._wait_time = 0.0
1187        self._update()
1188
1189        # Go with slightly more than 1 second to avoid timer stacking.
1190        self._update_timer = bs.Timer(
1191            1.1, bs.WeakCall(self._update), repeat=True
1192        )
1193
1194    def _update(self) -> None:
1195        tnt_alive = self._tnt is not None and self._tnt.node
1196        if not tnt_alive:
1197            # Respawn if its been long enough.. otherwise just increment our
1198            # how-long-since-we-died value.
1199            if self._tnt is None or self._wait_time >= self._respawn_time:
1200                self._tnt = Bomb(position=self._position, bomb_type='tnt')
1201                self._wait_time = 0.0
1202            else:
1203                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)
1181    def __init__(self, position: Sequence[float], respawn_time: float = 20.0):
1182        """Instantiate with given position and respawn_time (in seconds)."""
1183        self._position = position
1184        self._tnt: Bomb | None = None
1185        self._respawn_time = random.uniform(0.8, 1.2) * respawn_time
1186        self._wait_time = 0.0
1187        self._update()
1188
1189        # Go with slightly more than 1 second to avoid timer stacking.
1190        self._update_timer = bs.Timer(
1191            1.1, bs.WeakCall(self._update), repeat=True
1192        )

Instantiate with given position and respawn_time (in seconds).