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