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