bastd.actor.bomb
Various classes for bombs, mines, tnt, etc.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Various classes for bombs, mines, tnt, etc.""" 4 5# FIXME 6# pylint: disable=too-many-lines 7 8from __future__ import annotations 9 10import random 11from typing import TYPE_CHECKING, TypeVar 12 13import ba 14from bastd.gameutils import SharedObjects 15 16if TYPE_CHECKING: 17 from typing import Any, Sequence, Callable 18 19# pylint: disable=invalid-name 20PlayerType = TypeVar('PlayerType', bound='ba.Player') 21# pylint: enable=invalid-name 22 23 24class BombFactory: 25 """Wraps up media and other resources used by ba.Bombs. 26 27 Category: **Gameplay Classes** 28 29 A single instance of this is shared between all bombs 30 and can be retrieved via bastd.actor.bomb.get_factory(). 31 """ 32 33 bomb_model: ba.Model 34 """The ba.Model of a standard or ice bomb.""" 35 36 sticky_bomb_model: ba.Model 37 """The ba.Model of a sticky-bomb.""" 38 39 impact_bomb_model: ba.Model 40 """The ba.Model of an impact-bomb.""" 41 42 land_mine_model: ba.Model 43 """The ba.Model of a land-mine.""" 44 45 tnt_model: ba.Model 46 """The ba.Model of a tnt box.""" 47 48 regular_tex: ba.Texture 49 """The ba.Texture for regular bombs.""" 50 51 ice_tex: ba.Texture 52 """The ba.Texture for ice bombs.""" 53 54 sticky_tex: ba.Texture 55 """The ba.Texture for sticky bombs.""" 56 57 impact_tex: ba.Texture 58 """The ba.Texture for impact bombs.""" 59 60 impact_lit_tex: ba.Texture 61 """The ba.Texture for impact bombs with lights lit.""" 62 63 land_mine_tex: ba.Texture 64 """The ba.Texture for land-mines.""" 65 66 land_mine_lit_tex: ba.Texture 67 """The ba.Texture for land-mines with the light lit.""" 68 69 tnt_tex: ba.Texture 70 """The ba.Texture for tnt boxes.""" 71 72 hiss_sound: ba.Sound 73 """The ba.Sound for the hiss sound an ice bomb makes.""" 74 75 debris_fall_sound: ba.Sound 76 """The ba.Sound for random falling debris after an explosion.""" 77 78 wood_debris_fall_sound: ba.Sound 79 """A ba.Sound for random wood debris falling after an explosion.""" 80 81 explode_sounds: Sequence[ba.Sound] 82 """A tuple of ba.Sound-s for explosions.""" 83 84 freeze_sound: ba.Sound 85 """A ba.Sound of an ice bomb freezing something.""" 86 87 fuse_sound: ba.Sound 88 """A ba.Sound of a burning fuse.""" 89 90 activate_sound: ba.Sound 91 """A ba.Sound for an activating impact bomb.""" 92 93 warn_sound: ba.Sound 94 """A ba.Sound for an impact bomb about to explode due to time-out.""" 95 96 bomb_material: ba.Material 97 """A ba.Material applied to all bombs.""" 98 99 normal_sound_material: ba.Material 100 """A ba.Material that generates standard bomb noises on impacts, etc.""" 101 102 sticky_material: ba.Material 103 """A ba.Material that makes 'splat' sounds and makes collisions softer.""" 104 105 land_mine_no_explode_material: ba.Material 106 """A ba.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: ba.Material 111 """A ba.Material applied to activated land-mines that causes them to 112 explode on impact.""" 113 114 impact_blast_material: ba.Material 115 """A ba.Material applied to activated impact-bombs that causes them to 116 explode on impact.""" 117 118 blast_material: ba.Material 119 """A ba.Material applied to bomb blast geometry which triggers impact 120 events with what it touches.""" 121 122 dink_sounds: Sequence[ba.Sound] 123 """A tuple of ba.Sound-s for when bombs hit the ground.""" 124 125 sticky_impact_sound: ba.Sound 126 """The ba.Sound for a squish made by a sticky bomb hitting something.""" 127 128 roll_sound: ba.Sound 129 """ba.Sound for a rolling bomb.""" 130 131 _STORENAME = ba.storagename() 132 133 @classmethod 134 def get(cls) -> BombFactory: 135 """Get/create a shared bastd.actor.bomb.BombFactory object.""" 136 activity = ba.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) -> ba.Sound: 145 """Return a random explosion ba.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 bastd.actor.bomb.get_factory() 152 to get a shared instance. 153 """ 154 shared = SharedObjects.get() 155 156 self.bomb_model = ba.getmodel('bomb') 157 self.sticky_bomb_model = ba.getmodel('bombSticky') 158 self.impact_bomb_model = ba.getmodel('impactBomb') 159 self.land_mine_model = ba.getmodel('landMine') 160 self.tnt_model = ba.getmodel('tnt') 161 162 self.regular_tex = ba.gettexture('bombColor') 163 self.ice_tex = ba.gettexture('bombColorIce') 164 self.sticky_tex = ba.gettexture('bombStickyColor') 165 self.impact_tex = ba.gettexture('impactBombColor') 166 self.impact_lit_tex = ba.gettexture('impactBombColorLit') 167 self.land_mine_tex = ba.gettexture('landMine') 168 self.land_mine_lit_tex = ba.gettexture('landMineLit') 169 self.tnt_tex = ba.gettexture('tnt') 170 171 self.hiss_sound = ba.getsound('hiss') 172 self.debris_fall_sound = ba.getsound('debrisFall') 173 self.wood_debris_fall_sound = ba.getsound('woodDebrisFall') 174 175 self.explode_sounds = ( 176 ba.getsound('explosion01'), 177 ba.getsound('explosion02'), 178 ba.getsound('explosion03'), 179 ba.getsound('explosion04'), 180 ba.getsound('explosion05'), 181 ) 182 183 self.freeze_sound = ba.getsound('freeze') 184 self.fuse_sound = ba.getsound('fuse01') 185 self.activate_sound = ba.getsound('activateBeep') 186 self.warn_sound = ba.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 = ba.Material() 191 self.normal_sound_material = ba.Material() 192 self.sticky_material = ba.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 = ba.Material() 219 self.land_mine_blast_material = ba.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 = ba.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 = ba.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 ba.getsound('bombDrop01'), 274 ba.getsound('bombDrop02'), 275 ) 276 self.sticky_impact_sound = ba.getsound('stickyImpact') 277 self.roll_sound = ba.getsound('bombRoll01') 278 279 # Collision sounds. 280 self.normal_sound_material.add_actions( 281 conditions=('they_have_material', shared.footing_material), 282 actions=( 283 ('impact_sound', self.dink_sounds, 2, 0.8), 284 ('roll_sound', self.roll_sound, 3, 6), 285 ), 286 ) 287 288 self.sticky_material.add_actions( 289 actions=( 290 ('modify_part_collision', 'stiffness', 0.1), 291 ('modify_part_collision', 'damping', 1.0), 292 ) 293 ) 294 295 self.sticky_material.add_actions( 296 conditions=( 297 ('they_have_material', shared.player_material), 298 'or', 299 ('they_have_material', shared.footing_material), 300 ), 301 actions=('message', 'our_node', 'at_connect', SplatMessage()), 302 ) 303 304 305class SplatMessage: 306 """Tells an object to make a splat noise.""" 307 308 309class ExplodeMessage: 310 """Tells an object to explode.""" 311 312 313class ImpactMessage: 314 """Tell an object it touched something.""" 315 316 317class ArmMessage: 318 """Tell an object to become armed.""" 319 320 321class WarnMessage: 322 """Tell an object to issue a warning sound.""" 323 324 325class ExplodeHitMessage: 326 """Tell an object it was hit by an explosion.""" 327 328 329class Blast(ba.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: ba.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 = ba.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 ba.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 = ba.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 ba.timer(1.0, explosion.delete) 392 393 if self.blast_type != 'ice': 394 ba.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 ba.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 ba.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 ba.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 ba.timer(0.05, emit) 430 431 elif self.blast_type == 'sticky': 432 433 def emit() -> None: 434 ba.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 ba.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 ba.emitfx( 450 position=position, 451 velocity=velocity, 452 count=15, 453 scale=0.6, 454 chunk_type='slime', 455 emit_type='stickers', 456 ) 457 ba.emitfx( 458 position=position, 459 velocity=velocity, 460 count=20, 461 scale=0.7, 462 chunk_type='spark', 463 emit_type='stickers', 464 ) 465 ba.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 ba.timer(0.05, emit) 476 477 elif self.blast_type == 'impact': 478 479 def emit() -> None: 480 ba.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 ba.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 ba.emitfx( 495 position=position, 496 velocity=velocity, 497 count=20, 498 scale=0.7, 499 chunk_type='spark', 500 emit_type='stickers', 501 ) 502 ba.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 ba.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 ba.emitfx( 519 position=position, 520 velocity=velocity, 521 count=int(4.0 + random.random() * 8), 522 chunk_type='rock', 523 ) 524 ba.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 ba.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 ba.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 ba.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 ba.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 ba.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 ba.timer(0.02, emit_extra_sparks) 577 578 # It looks better if we delay a bit. 579 ba.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 = ba.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 ba.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 ba.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 ba.timer(scl * 3.0, light.delete) 626 627 # Make a scorch that fades over time. 628 scorch = ba.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 ba.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) 640 ba.timer(13.0, scorch.delete) 641 642 if self.blast_type == 'ice': 643 ba.playsound(factory.hiss_sound, position=light.position) 644 645 lpos = light.position 646 ba.playsound(factory.random_explode_sound(), position=lpos) 647 ba.playsound(factory.debris_fall_sound, position=lpos) 648 649 ba.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 ba.playsound(factory.random_explode_sound(), position=lpos) 654 655 def _extra_boom() -> None: 656 ba.playsound(factory.random_explode_sound(), position=lpos) 657 658 ba.timer(0.25, _extra_boom) 659 660 def _extra_debris_sound() -> None: 661 ba.playsound(factory.debris_fall_sound, position=lpos) 662 ba.playsound(factory.wood_debris_fall_sound, position=lpos) 663 664 ba.timer(0.4, _extra_debris_sound) 665 666 def handlemessage(self, msg: Any) -> Any: 667 assert not self.expired 668 669 if isinstance(msg, ba.DieMessage): 670 if self.node: 671 self.node.delete() 672 673 elif isinstance(msg, ExplodeHitMessage): 674 node = ba.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 ba.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=ba.existing(self._source_player), 694 ) 695 ) 696 if self.blast_type == 'ice': 697 ba.playsound( 698 BombFactory.get().freeze_sound, 10, position=nodepos 699 ) 700 node.handlemessage(ba.FreezeMessage()) 701 702 else: 703 return super().handlemessage(msg) 704 return None 705 706 707class Bomb(ba.Actor): 708 """A standard bomb and its variants such as land-mines and tnt-boxes. 709 710 category: Gameplay Classes 711 """ 712 713 # Ew; should try to clean this up later. 714 # pylint: disable=too-many-locals 715 # pylint: disable=too-many-branches 716 # pylint: disable=too-many-statements 717 718 def __init__( 719 self, 720 position: Sequence[float] = (0.0, 1.0, 0.0), 721 velocity: Sequence[float] = (0.0, 0.0, 0.0), 722 bomb_type: str = 'normal', 723 blast_radius: float = 2.0, 724 bomb_scale: float = 1.0, 725 source_player: ba.Player | None = None, 726 owner: ba.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: ba.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[ba.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 = ba.newnode( 814 'prop', 815 delegate=self, 816 attrs={ 817 'position': position, 818 'velocity': velocity, 819 'model': factory.land_mine_model, 820 'light_model': factory.land_mine_model, 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 = ba.newnode( 834 'prop', 835 delegate=self, 836 attrs={ 837 'position': position, 838 'velocity': velocity, 839 'model': factory.tnt_model, 840 'light_model': factory.tnt_model, 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 = ba.newnode( 854 'prop', 855 delegate=self, 856 attrs={ 857 'position': position, 858 'velocity': velocity, 859 'body': 'sphere', 860 'body_scale': self.scale, 861 'model': factory.impact_bomb_model, 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 = ba.Timer( 870 0.2, ba.WeakCall(self.handlemessage, ArmMessage()) 871 ) 872 self.warn_timer = ba.Timer( 873 fuse_time - 1.7, ba.WeakCall(self.handlemessage, WarnMessage()) 874 ) 875 876 else: 877 fuse_time = 3.0 878 if self.bomb_type == 'sticky': 879 sticky = True 880 model = factory.sticky_bomb_model 881 rtype = 'sharper' 882 rscale = 1.8 883 else: 884 sticky = False 885 model = factory.bomb_model 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 = ba.newnode( 895 'bomb', 896 delegate=self, 897 attrs={ 898 'position': position, 899 'velocity': velocity, 900 'model': model, 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 = ba.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 ba.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 ba.timer( 924 fuse_time, ba.WeakCall(self.handlemessage, ExplodeMessage()) 925 ) 926 927 ba.animate( 928 self.node, 929 'model_scale', 930 {0: 0, 0.2: 1.3 * self.scale, 0.26: self.scale}, 931 ) 932 933 def get_source_player( 934 self, playertype: type[PlayerType] 935 ) -> PlayerType | None: 936 """Return the source-player if one exists and is the provided type.""" 937 player: Any = self._source_player 938 return ( 939 player 940 if isinstance(player, playertype) and player.exists() 941 else None 942 ) 943 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(ba.DieMessage()) 956 957 def _handle_impact(self) -> None: 958 node = ba.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 = ba.Timer( 981 1.25, ba.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: ba.Node) -> None: 988 if node: 989 node.stick_to_owner = True 990 991 ba.timer(0.25, lambda: _setsticky(self.node)) 992 993 def _handle_splat(self) -> None: 994 node = ba.getcollision().opposingnode 995 if ( 996 node is not self.owner 997 and ba.time() - self._last_sticky_sound_time > 1.0 998 ): 999 self._last_sticky_sound_time = ba.time() 1000 assert self.node 1001 ba.playsound( 1002 BombFactory.get().sticky_impact_sound, 1003 2.0, 1004 position=self.node.position, 1005 ) 1006 1007 def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None: 1008 """Add a call to be run when the bomb has exploded. 1009 1010 The bomb and the new blast object are passed as arguments. 1011 """ 1012 self._explode_callbacks.append(call) 1013 1014 def explode(self) -> None: 1015 """Blows up the bomb if it has not yet done so.""" 1016 if self._exploded: 1017 return 1018 self._exploded = True 1019 if self.node: 1020 blast = Blast( 1021 position=self.node.position, 1022 velocity=self.node.velocity, 1023 blast_radius=self.blast_radius, 1024 blast_type=self.bomb_type, 1025 source_player=ba.existing(self._source_player), 1026 hit_type=self.hit_type, 1027 hit_subtype=self.hit_subtype, 1028 ).autoretain() 1029 for callback in self._explode_callbacks: 1030 callback(self, blast) 1031 1032 # We blew up so we need to go away. 1033 # NOTE TO SELF: do we actually need this delay? 1034 ba.timer(0.001, ba.WeakCall(self.handlemessage, ba.DieMessage())) 1035 1036 def _handle_warn(self) -> None: 1037 if self.texture_sequence and self.node: 1038 self.texture_sequence.rate = 30 1039 ba.playsound( 1040 BombFactory.get().warn_sound, 0.5, position=self.node.position 1041 ) 1042 1043 def _add_material(self, material: ba.Material) -> None: 1044 if not self.node: 1045 return 1046 materials = self.node.materials 1047 if material not in materials: 1048 assert isinstance(materials, tuple) 1049 self.node.materials = materials + (material,) 1050 1051 def arm(self) -> None: 1052 """Arm the bomb (for land-mines and impact-bombs). 1053 1054 These types of bombs will not explode until they have been armed. 1055 """ 1056 if not self.node: 1057 return 1058 factory = BombFactory.get() 1059 intex: Sequence[ba.Texture] 1060 if self.bomb_type == 'land_mine': 1061 intex = (factory.land_mine_lit_tex, factory.land_mine_tex) 1062 self.texture_sequence = ba.newnode( 1063 'texture_sequence', 1064 owner=self.node, 1065 attrs={'rate': 30, 'input_textures': intex}, 1066 ) 1067 ba.timer(0.5, self.texture_sequence.delete) 1068 1069 # We now make it explodable. 1070 ba.timer( 1071 0.25, 1072 ba.WeakCall( 1073 self._add_material, factory.land_mine_blast_material 1074 ), 1075 ) 1076 elif self.bomb_type == 'impact': 1077 intex = ( 1078 factory.impact_lit_tex, 1079 factory.impact_tex, 1080 factory.impact_tex, 1081 ) 1082 self.texture_sequence = ba.newnode( 1083 'texture_sequence', 1084 owner=self.node, 1085 attrs={'rate': 100, 'input_textures': intex}, 1086 ) 1087 ba.timer( 1088 0.25, 1089 ba.WeakCall( 1090 self._add_material, factory.land_mine_blast_material 1091 ), 1092 ) 1093 else: 1094 raise RuntimeError( 1095 'arm() should only be called on land-mines or impact bombs' 1096 ) 1097 self.texture_sequence.connectattr( 1098 'output_texture', self.node, 'color_texture' 1099 ) 1100 ba.playsound(factory.activate_sound, 0.5, position=self.node.position) 1101 1102 def _handle_hit(self, msg: ba.HitMessage) -> None: 1103 ispunched = msg.srcnode and msg.srcnode.getnodetype() == 'spaz' 1104 1105 # Normal bombs are triggered by non-punch impacts; 1106 # impact-bombs by all impacts. 1107 if not self._exploded and ( 1108 not ispunched or self.bomb_type in ['impact', 'land_mine'] 1109 ): 1110 1111 # Also lets change the owner of the bomb to whoever is setting 1112 # us off. (this way points for big chain reactions go to the 1113 # person causing them). 1114 source_player = msg.get_source_player(ba.Player) 1115 if source_player is not None: 1116 self._source_player = source_player 1117 1118 # Also inherit the hit type (if a landmine sets off by a bomb, 1119 # the credit should go to the mine) 1120 # the exception is TNT. TNT always gets credit. 1121 # UPDATE (July 2020): not doing this anymore. Causes too much 1122 # weird logic such as bombs acting like punches. Holler if 1123 # anything is noticeably broken due to this. 1124 # if self.bomb_type != 'tnt': 1125 # self.hit_type = msg.hit_type 1126 # self.hit_subtype = msg.hit_subtype 1127 1128 ba.timer( 1129 0.1 + random.random() * 0.1, 1130 ba.WeakCall(self.handlemessage, ExplodeMessage()), 1131 ) 1132 assert self.node 1133 self.node.handlemessage( 1134 'impulse', 1135 msg.pos[0], 1136 msg.pos[1], 1137 msg.pos[2], 1138 msg.velocity[0], 1139 msg.velocity[1], 1140 msg.velocity[2], 1141 msg.magnitude, 1142 msg.velocity_magnitude, 1143 msg.radius, 1144 0, 1145 msg.velocity[0], 1146 msg.velocity[1], 1147 msg.velocity[2], 1148 ) 1149 1150 if msg.srcnode: 1151 pass 1152 1153 def handlemessage(self, msg: Any) -> Any: 1154 if isinstance(msg, ExplodeMessage): 1155 self.explode() 1156 elif isinstance(msg, ImpactMessage): 1157 self._handle_impact() 1158 # Ok the logic below looks like it was backwards to me. 1159 # Disabling for now; can bring back if need be. 1160 # elif isinstance(msg, ba.PickedUpMessage): 1161 # # Change our source to whoever just picked us up *only* if it 1162 # # is None. This way we can get points for killing bots with their 1163 # # own bombs. Hmm would there be a downside to this? 1164 # if self._source_player is not None: 1165 # self._source_player = msg.node.source_player 1166 elif isinstance(msg, SplatMessage): 1167 self._handle_splat() 1168 elif isinstance(msg, ba.DroppedMessage): 1169 self._handle_dropped() 1170 elif isinstance(msg, ba.HitMessage): 1171 self._handle_hit(msg) 1172 elif isinstance(msg, ba.DieMessage): 1173 self._handle_die() 1174 elif isinstance(msg, ba.OutOfBoundsMessage): 1175 self._handle_oob() 1176 elif isinstance(msg, ArmMessage): 1177 self.arm() 1178 elif isinstance(msg, WarnMessage): 1179 self._handle_warn() 1180 else: 1181 super().handlemessage(msg) 1182 1183 1184class TNTSpawner: 1185 """Regenerates TNT at a given point in space every now and then. 1186 1187 category: Gameplay Classes 1188 """ 1189 1190 def __init__(self, position: Sequence[float], respawn_time: float = 20.0): 1191 """Instantiate with given position and respawn_time (in seconds).""" 1192 self._position = position 1193 self._tnt: Bomb | None = None 1194 self._respawn_time = random.uniform(0.8, 1.2) * respawn_time 1195 self._wait_time = 0.0 1196 self._update() 1197 1198 # Go with slightly more than 1 second to avoid timer stacking. 1199 self._update_timer = ba.Timer( 1200 1.1, ba.WeakCall(self._update), repeat=True 1201 ) 1202 1203 def _update(self) -> None: 1204 tnt_alive = self._tnt is not None and self._tnt.node 1205 if not tnt_alive: 1206 # Respawn if its been long enough.. otherwise just increment our 1207 # how-long-since-we-died value. 1208 if self._tnt is None or self._wait_time >= self._respawn_time: 1209 self._tnt = Bomb(position=self._position, bomb_type='tnt') 1210 self._wait_time = 0.0 1211 else: 1212 self._wait_time += 1.1
25class BombFactory: 26 """Wraps up media and other resources used by ba.Bombs. 27 28 Category: **Gameplay Classes** 29 30 A single instance of this is shared between all bombs 31 and can be retrieved via bastd.actor.bomb.get_factory(). 32 """ 33 34 bomb_model: ba.Model 35 """The ba.Model of a standard or ice bomb.""" 36 37 sticky_bomb_model: ba.Model 38 """The ba.Model of a sticky-bomb.""" 39 40 impact_bomb_model: ba.Model 41 """The ba.Model of an impact-bomb.""" 42 43 land_mine_model: ba.Model 44 """The ba.Model of a land-mine.""" 45 46 tnt_model: ba.Model 47 """The ba.Model of a tnt box.""" 48 49 regular_tex: ba.Texture 50 """The ba.Texture for regular bombs.""" 51 52 ice_tex: ba.Texture 53 """The ba.Texture for ice bombs.""" 54 55 sticky_tex: ba.Texture 56 """The ba.Texture for sticky bombs.""" 57 58 impact_tex: ba.Texture 59 """The ba.Texture for impact bombs.""" 60 61 impact_lit_tex: ba.Texture 62 """The ba.Texture for impact bombs with lights lit.""" 63 64 land_mine_tex: ba.Texture 65 """The ba.Texture for land-mines.""" 66 67 land_mine_lit_tex: ba.Texture 68 """The ba.Texture for land-mines with the light lit.""" 69 70 tnt_tex: ba.Texture 71 """The ba.Texture for tnt boxes.""" 72 73 hiss_sound: ba.Sound 74 """The ba.Sound for the hiss sound an ice bomb makes.""" 75 76 debris_fall_sound: ba.Sound 77 """The ba.Sound for random falling debris after an explosion.""" 78 79 wood_debris_fall_sound: ba.Sound 80 """A ba.Sound for random wood debris falling after an explosion.""" 81 82 explode_sounds: Sequence[ba.Sound] 83 """A tuple of ba.Sound-s for explosions.""" 84 85 freeze_sound: ba.Sound 86 """A ba.Sound of an ice bomb freezing something.""" 87 88 fuse_sound: ba.Sound 89 """A ba.Sound of a burning fuse.""" 90 91 activate_sound: ba.Sound 92 """A ba.Sound for an activating impact bomb.""" 93 94 warn_sound: ba.Sound 95 """A ba.Sound for an impact bomb about to explode due to time-out.""" 96 97 bomb_material: ba.Material 98 """A ba.Material applied to all bombs.""" 99 100 normal_sound_material: ba.Material 101 """A ba.Material that generates standard bomb noises on impacts, etc.""" 102 103 sticky_material: ba.Material 104 """A ba.Material that makes 'splat' sounds and makes collisions softer.""" 105 106 land_mine_no_explode_material: ba.Material 107 """A ba.Material that keeps land-mines from blowing up. 108 Applied to land-mines when they are created to allow land-mines to 109 touch without exploding.""" 110 111 land_mine_blast_material: ba.Material 112 """A ba.Material applied to activated land-mines that causes them to 113 explode on impact.""" 114 115 impact_blast_material: ba.Material 116 """A ba.Material applied to activated impact-bombs that causes them to 117 explode on impact.""" 118 119 blast_material: ba.Material 120 """A ba.Material applied to bomb blast geometry which triggers impact 121 events with what it touches.""" 122 123 dink_sounds: Sequence[ba.Sound] 124 """A tuple of ba.Sound-s for when bombs hit the ground.""" 125 126 sticky_impact_sound: ba.Sound 127 """The ba.Sound for a squish made by a sticky bomb hitting something.""" 128 129 roll_sound: ba.Sound 130 """ba.Sound for a rolling bomb.""" 131 132 _STORENAME = ba.storagename() 133 134 @classmethod 135 def get(cls) -> BombFactory: 136 """Get/create a shared bastd.actor.bomb.BombFactory object.""" 137 activity = ba.getactivity() 138 factory = activity.customdata.get(cls._STORENAME) 139 if factory is None: 140 factory = BombFactory() 141 activity.customdata[cls._STORENAME] = factory 142 assert isinstance(factory, BombFactory) 143 return factory 144 145 def random_explode_sound(self) -> ba.Sound: 146 """Return a random explosion ba.Sound from the factory.""" 147 return self.explode_sounds[random.randrange(len(self.explode_sounds))] 148 149 def __init__(self) -> None: 150 """Instantiate a BombFactory. 151 152 You shouldn't need to do this; call bastd.actor.bomb.get_factory() 153 to get a shared instance. 154 """ 155 shared = SharedObjects.get() 156 157 self.bomb_model = ba.getmodel('bomb') 158 self.sticky_bomb_model = ba.getmodel('bombSticky') 159 self.impact_bomb_model = ba.getmodel('impactBomb') 160 self.land_mine_model = ba.getmodel('landMine') 161 self.tnt_model = ba.getmodel('tnt') 162 163 self.regular_tex = ba.gettexture('bombColor') 164 self.ice_tex = ba.gettexture('bombColorIce') 165 self.sticky_tex = ba.gettexture('bombStickyColor') 166 self.impact_tex = ba.gettexture('impactBombColor') 167 self.impact_lit_tex = ba.gettexture('impactBombColorLit') 168 self.land_mine_tex = ba.gettexture('landMine') 169 self.land_mine_lit_tex = ba.gettexture('landMineLit') 170 self.tnt_tex = ba.gettexture('tnt') 171 172 self.hiss_sound = ba.getsound('hiss') 173 self.debris_fall_sound = ba.getsound('debrisFall') 174 self.wood_debris_fall_sound = ba.getsound('woodDebrisFall') 175 176 self.explode_sounds = ( 177 ba.getsound('explosion01'), 178 ba.getsound('explosion02'), 179 ba.getsound('explosion03'), 180 ba.getsound('explosion04'), 181 ba.getsound('explosion05'), 182 ) 183 184 self.freeze_sound = ba.getsound('freeze') 185 self.fuse_sound = ba.getsound('fuse01') 186 self.activate_sound = ba.getsound('activateBeep') 187 self.warn_sound = ba.getsound('warnBeep') 188 189 # Set up our material so new bombs don't collide with objects 190 # that they are initially overlapping. 191 self.bomb_material = ba.Material() 192 self.normal_sound_material = ba.Material() 193 self.sticky_material = ba.Material() 194 195 self.bomb_material.add_actions( 196 conditions=( 197 ( 198 ('we_are_younger_than', 100), 199 'or', 200 ('they_are_younger_than', 100), 201 ), 202 'and', 203 ('they_have_material', shared.object_material), 204 ), 205 actions=('modify_node_collision', 'collide', False), 206 ) 207 208 # We want pickup materials to always hit us even if we're currently 209 # not colliding with their node. (generally due to the above rule) 210 self.bomb_material.add_actions( 211 conditions=('they_have_material', shared.pickup_material), 212 actions=('modify_part_collision', 'use_node_collide', False), 213 ) 214 215 self.bomb_material.add_actions( 216 actions=('modify_part_collision', 'friction', 0.3) 217 ) 218 219 self.land_mine_no_explode_material = ba.Material() 220 self.land_mine_blast_material = ba.Material() 221 self.land_mine_blast_material.add_actions( 222 conditions=( 223 ('we_are_older_than', 200), 224 'and', 225 ('they_are_older_than', 200), 226 'and', 227 ('eval_colliding',), 228 'and', 229 ( 230 ( 231 'they_dont_have_material', 232 self.land_mine_no_explode_material, 233 ), 234 'and', 235 ( 236 ('they_have_material', shared.object_material), 237 'or', 238 ('they_have_material', shared.player_material), 239 ), 240 ), 241 ), 242 actions=('message', 'our_node', 'at_connect', ImpactMessage()), 243 ) 244 245 self.impact_blast_material = ba.Material() 246 self.impact_blast_material.add_actions( 247 conditions=( 248 ('we_are_older_than', 200), 249 'and', 250 ('they_are_older_than', 200), 251 'and', 252 ('eval_colliding',), 253 'and', 254 ( 255 ('they_have_material', shared.footing_material), 256 'or', 257 ('they_have_material', shared.object_material), 258 ), 259 ), 260 actions=('message', 'our_node', 'at_connect', ImpactMessage()), 261 ) 262 263 self.blast_material = ba.Material() 264 self.blast_material.add_actions( 265 conditions=('they_have_material', shared.object_material), 266 actions=( 267 ('modify_part_collision', 'collide', True), 268 ('modify_part_collision', 'physical', False), 269 ('message', 'our_node', 'at_connect', ExplodeHitMessage()), 270 ), 271 ) 272 273 self.dink_sounds = ( 274 ba.getsound('bombDrop01'), 275 ba.getsound('bombDrop02'), 276 ) 277 self.sticky_impact_sound = ba.getsound('stickyImpact') 278 self.roll_sound = ba.getsound('bombRoll01') 279 280 # Collision sounds. 281 self.normal_sound_material.add_actions( 282 conditions=('they_have_material', shared.footing_material), 283 actions=( 284 ('impact_sound', self.dink_sounds, 2, 0.8), 285 ('roll_sound', self.roll_sound, 3, 6), 286 ), 287 ) 288 289 self.sticky_material.add_actions( 290 actions=( 291 ('modify_part_collision', 'stiffness', 0.1), 292 ('modify_part_collision', 'damping', 1.0), 293 ) 294 ) 295 296 self.sticky_material.add_actions( 297 conditions=( 298 ('they_have_material', shared.player_material), 299 'or', 300 ('they_have_material', shared.footing_material), 301 ), 302 actions=('message', 'our_node', 'at_connect', SplatMessage()), 303 )
Wraps up media and other resources used by ba.Bombs.
Category: Gameplay Classes
A single instance of this is shared between all bombs and can be retrieved via bastd.actor.bomb.get_factory().
149 def __init__(self) -> None: 150 """Instantiate a BombFactory. 151 152 You shouldn't need to do this; call bastd.actor.bomb.get_factory() 153 to get a shared instance. 154 """ 155 shared = SharedObjects.get() 156 157 self.bomb_model = ba.getmodel('bomb') 158 self.sticky_bomb_model = ba.getmodel('bombSticky') 159 self.impact_bomb_model = ba.getmodel('impactBomb') 160 self.land_mine_model = ba.getmodel('landMine') 161 self.tnt_model = ba.getmodel('tnt') 162 163 self.regular_tex = ba.gettexture('bombColor') 164 self.ice_tex = ba.gettexture('bombColorIce') 165 self.sticky_tex = ba.gettexture('bombStickyColor') 166 self.impact_tex = ba.gettexture('impactBombColor') 167 self.impact_lit_tex = ba.gettexture('impactBombColorLit') 168 self.land_mine_tex = ba.gettexture('landMine') 169 self.land_mine_lit_tex = ba.gettexture('landMineLit') 170 self.tnt_tex = ba.gettexture('tnt') 171 172 self.hiss_sound = ba.getsound('hiss') 173 self.debris_fall_sound = ba.getsound('debrisFall') 174 self.wood_debris_fall_sound = ba.getsound('woodDebrisFall') 175 176 self.explode_sounds = ( 177 ba.getsound('explosion01'), 178 ba.getsound('explosion02'), 179 ba.getsound('explosion03'), 180 ba.getsound('explosion04'), 181 ba.getsound('explosion05'), 182 ) 183 184 self.freeze_sound = ba.getsound('freeze') 185 self.fuse_sound = ba.getsound('fuse01') 186 self.activate_sound = ba.getsound('activateBeep') 187 self.warn_sound = ba.getsound('warnBeep') 188 189 # Set up our material so new bombs don't collide with objects 190 # that they are initially overlapping. 191 self.bomb_material = ba.Material() 192 self.normal_sound_material = ba.Material() 193 self.sticky_material = ba.Material() 194 195 self.bomb_material.add_actions( 196 conditions=( 197 ( 198 ('we_are_younger_than', 100), 199 'or', 200 ('they_are_younger_than', 100), 201 ), 202 'and', 203 ('they_have_material', shared.object_material), 204 ), 205 actions=('modify_node_collision', 'collide', False), 206 ) 207 208 # We want pickup materials to always hit us even if we're currently 209 # not colliding with their node. (generally due to the above rule) 210 self.bomb_material.add_actions( 211 conditions=('they_have_material', shared.pickup_material), 212 actions=('modify_part_collision', 'use_node_collide', False), 213 ) 214 215 self.bomb_material.add_actions( 216 actions=('modify_part_collision', 'friction', 0.3) 217 ) 218 219 self.land_mine_no_explode_material = ba.Material() 220 self.land_mine_blast_material = ba.Material() 221 self.land_mine_blast_material.add_actions( 222 conditions=( 223 ('we_are_older_than', 200), 224 'and', 225 ('they_are_older_than', 200), 226 'and', 227 ('eval_colliding',), 228 'and', 229 ( 230 ( 231 'they_dont_have_material', 232 self.land_mine_no_explode_material, 233 ), 234 'and', 235 ( 236 ('they_have_material', shared.object_material), 237 'or', 238 ('they_have_material', shared.player_material), 239 ), 240 ), 241 ), 242 actions=('message', 'our_node', 'at_connect', ImpactMessage()), 243 ) 244 245 self.impact_blast_material = ba.Material() 246 self.impact_blast_material.add_actions( 247 conditions=( 248 ('we_are_older_than', 200), 249 'and', 250 ('they_are_older_than', 200), 251 'and', 252 ('eval_colliding',), 253 'and', 254 ( 255 ('they_have_material', shared.footing_material), 256 'or', 257 ('they_have_material', shared.object_material), 258 ), 259 ), 260 actions=('message', 'our_node', 'at_connect', ImpactMessage()), 261 ) 262 263 self.blast_material = ba.Material() 264 self.blast_material.add_actions( 265 conditions=('they_have_material', shared.object_material), 266 actions=( 267 ('modify_part_collision', 'collide', True), 268 ('modify_part_collision', 'physical', False), 269 ('message', 'our_node', 'at_connect', ExplodeHitMessage()), 270 ), 271 ) 272 273 self.dink_sounds = ( 274 ba.getsound('bombDrop01'), 275 ba.getsound('bombDrop02'), 276 ) 277 self.sticky_impact_sound = ba.getsound('stickyImpact') 278 self.roll_sound = ba.getsound('bombRoll01') 279 280 # Collision sounds. 281 self.normal_sound_material.add_actions( 282 conditions=('they_have_material', shared.footing_material), 283 actions=( 284 ('impact_sound', self.dink_sounds, 2, 0.8), 285 ('roll_sound', self.roll_sound, 3, 6), 286 ), 287 ) 288 289 self.sticky_material.add_actions( 290 actions=( 291 ('modify_part_collision', 'stiffness', 0.1), 292 ('modify_part_collision', 'damping', 1.0), 293 ) 294 ) 295 296 self.sticky_material.add_actions( 297 conditions=( 298 ('they_have_material', shared.player_material), 299 'or', 300 ('they_have_material', shared.footing_material), 301 ), 302 actions=('message', 'our_node', 'at_connect', SplatMessage()), 303 )
Instantiate a BombFactory.
You shouldn't need to do this; call bastd.actor.bomb.get_factory() to get a shared instance.
A ba.Material that generates standard bomb noises on impacts, etc.
A ba.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 ba.Material applied to activated land-mines that causes them to explode on impact.
A ba.Material applied to activated impact-bombs that causes them to explode on impact.
A ba.Material applied to bomb blast geometry which triggers impact events with what it touches.
134 @classmethod 135 def get(cls) -> BombFactory: 136 """Get/create a shared bastd.actor.bomb.BombFactory object.""" 137 activity = ba.getactivity() 138 factory = activity.customdata.get(cls._STORENAME) 139 if factory is None: 140 factory = BombFactory() 141 activity.customdata[cls._STORENAME] = factory 142 assert isinstance(factory, BombFactory) 143 return factory
Get/create a shared bastd.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.
330class Blast(ba.Actor): 331 """An explosion, as generated by a bomb or some other object. 332 333 category: Gameplay Classes 334 """ 335 336 def __init__( 337 self, 338 position: Sequence[float] = (0.0, 1.0, 0.0), 339 velocity: Sequence[float] = (0.0, 0.0, 0.0), 340 blast_radius: float = 2.0, 341 blast_type: str = 'normal', 342 source_player: ba.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 = ba.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 ba.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 = ba.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 ba.timer(1.0, explosion.delete) 393 394 if self.blast_type != 'ice': 395 ba.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 ba.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 ba.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 ba.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 ba.timer(0.05, emit) 431 432 elif self.blast_type == 'sticky': 433 434 def emit() -> None: 435 ba.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 ba.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 ba.emitfx( 451 position=position, 452 velocity=velocity, 453 count=15, 454 scale=0.6, 455 chunk_type='slime', 456 emit_type='stickers', 457 ) 458 ba.emitfx( 459 position=position, 460 velocity=velocity, 461 count=20, 462 scale=0.7, 463 chunk_type='spark', 464 emit_type='stickers', 465 ) 466 ba.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 ba.timer(0.05, emit) 477 478 elif self.blast_type == 'impact': 479 480 def emit() -> None: 481 ba.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 ba.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 ba.emitfx( 496 position=position, 497 velocity=velocity, 498 count=20, 499 scale=0.7, 500 chunk_type='spark', 501 emit_type='stickers', 502 ) 503 ba.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 ba.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 ba.emitfx( 520 position=position, 521 velocity=velocity, 522 count=int(4.0 + random.random() * 8), 523 chunk_type='rock', 524 ) 525 ba.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 ba.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 ba.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 ba.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 ba.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 ba.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 ba.timer(0.02, emit_extra_sparks) 578 579 # It looks better if we delay a bit. 580 ba.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 = ba.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 ba.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 ba.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 ba.timer(scl * 3.0, light.delete) 627 628 # Make a scorch that fades over time. 629 scorch = ba.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 ba.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) 641 ba.timer(13.0, scorch.delete) 642 643 if self.blast_type == 'ice': 644 ba.playsound(factory.hiss_sound, position=light.position) 645 646 lpos = light.position 647 ba.playsound(factory.random_explode_sound(), position=lpos) 648 ba.playsound(factory.debris_fall_sound, position=lpos) 649 650 ba.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 ba.playsound(factory.random_explode_sound(), position=lpos) 655 656 def _extra_boom() -> None: 657 ba.playsound(factory.random_explode_sound(), position=lpos) 658 659 ba.timer(0.25, _extra_boom) 660 661 def _extra_debris_sound() -> None: 662 ba.playsound(factory.debris_fall_sound, position=lpos) 663 ba.playsound(factory.wood_debris_fall_sound, position=lpos) 664 665 ba.timer(0.4, _extra_debris_sound) 666 667 def handlemessage(self, msg: Any) -> Any: 668 assert not self.expired 669 670 if isinstance(msg, ba.DieMessage): 671 if self.node: 672 self.node.delete() 673 674 elif isinstance(msg, ExplodeHitMessage): 675 node = ba.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 ba.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=ba.existing(self._source_player), 695 ) 696 ) 697 if self.blast_type == 'ice': 698 ba.playsound( 699 BombFactory.get().freeze_sound, 10, position=nodepos 700 ) 701 node.handlemessage(ba.FreezeMessage()) 702 703 else: 704 return super().handlemessage(msg) 705 return None
An explosion, as generated by a bomb or some other object.
category: Gameplay Classes
336 def __init__( 337 self, 338 position: Sequence[float] = (0.0, 1.0, 0.0), 339 velocity: Sequence[float] = (0.0, 0.0, 0.0), 340 blast_radius: float = 2.0, 341 blast_type: str = 'normal', 342 source_player: ba.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 = ba.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 ba.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 = ba.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 ba.timer(1.0, explosion.delete) 393 394 if self.blast_type != 'ice': 395 ba.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 ba.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 ba.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 ba.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 ba.timer(0.05, emit) 431 432 elif self.blast_type == 'sticky': 433 434 def emit() -> None: 435 ba.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 ba.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 ba.emitfx( 451 position=position, 452 velocity=velocity, 453 count=15, 454 scale=0.6, 455 chunk_type='slime', 456 emit_type='stickers', 457 ) 458 ba.emitfx( 459 position=position, 460 velocity=velocity, 461 count=20, 462 scale=0.7, 463 chunk_type='spark', 464 emit_type='stickers', 465 ) 466 ba.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 ba.timer(0.05, emit) 477 478 elif self.blast_type == 'impact': 479 480 def emit() -> None: 481 ba.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 ba.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 ba.emitfx( 496 position=position, 497 velocity=velocity, 498 count=20, 499 scale=0.7, 500 chunk_type='spark', 501 emit_type='stickers', 502 ) 503 ba.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 ba.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 ba.emitfx( 520 position=position, 521 velocity=velocity, 522 count=int(4.0 + random.random() * 8), 523 chunk_type='rock', 524 ) 525 ba.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 ba.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 ba.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 ba.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 ba.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 ba.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 ba.timer(0.02, emit_extra_sparks) 578 579 # It looks better if we delay a bit. 580 ba.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 = ba.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 ba.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 ba.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 ba.timer(scl * 3.0, light.delete) 627 628 # Make a scorch that fades over time. 629 scorch = ba.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 ba.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) 641 ba.timer(13.0, scorch.delete) 642 643 if self.blast_type == 'ice': 644 ba.playsound(factory.hiss_sound, position=light.position) 645 646 lpos = light.position 647 ba.playsound(factory.random_explode_sound(), position=lpos) 648 ba.playsound(factory.debris_fall_sound, position=lpos) 649 650 ba.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 ba.playsound(factory.random_explode_sound(), position=lpos) 655 656 def _extra_boom() -> None: 657 ba.playsound(factory.random_explode_sound(), position=lpos) 658 659 ba.timer(0.25, _extra_boom) 660 661 def _extra_debris_sound() -> None: 662 ba.playsound(factory.debris_fall_sound, position=lpos) 663 ba.playsound(factory.wood_debris_fall_sound, position=lpos) 664 665 ba.timer(0.4, _extra_debris_sound)
Instantiate with given values.
667 def handlemessage(self, msg: Any) -> Any: 668 assert not self.expired 669 670 if isinstance(msg, ba.DieMessage): 671 if self.node: 672 self.node.delete() 673 674 elif isinstance(msg, ExplodeHitMessage): 675 node = ba.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 ba.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=ba.existing(self._source_player), 695 ) 696 ) 697 if self.blast_type == 'ice': 698 ba.playsound( 699 BombFactory.get().freeze_sound, 10, position=nodepos 700 ) 701 node.handlemessage(ba.FreezeMessage()) 702 703 else: 704 return super().handlemessage(msg) 705 return None
General message handling; can be passed any message object.
Inherited Members
- ba._actor.Actor
- autoretain
- on_expire
- expired
- exists
- is_alive
- activity
- getactivity
708class Bomb(ba.Actor): 709 """A standard bomb and its variants such as land-mines and tnt-boxes. 710 711 category: Gameplay Classes 712 """ 713 714 # Ew; should try to clean this up later. 715 # pylint: disable=too-many-locals 716 # pylint: disable=too-many-branches 717 # pylint: disable=too-many-statements 718 719 def __init__( 720 self, 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: ba.Player | None = None, 727 owner: ba.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: ba.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[ba.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 = ba.newnode( 815 'prop', 816 delegate=self, 817 attrs={ 818 'position': position, 819 'velocity': velocity, 820 'model': factory.land_mine_model, 821 'light_model': factory.land_mine_model, 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 = ba.newnode( 835 'prop', 836 delegate=self, 837 attrs={ 838 'position': position, 839 'velocity': velocity, 840 'model': factory.tnt_model, 841 'light_model': factory.tnt_model, 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 = ba.newnode( 855 'prop', 856 delegate=self, 857 attrs={ 858 'position': position, 859 'velocity': velocity, 860 'body': 'sphere', 861 'body_scale': self.scale, 862 'model': factory.impact_bomb_model, 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 = ba.Timer( 871 0.2, ba.WeakCall(self.handlemessage, ArmMessage()) 872 ) 873 self.warn_timer = ba.Timer( 874 fuse_time - 1.7, ba.WeakCall(self.handlemessage, WarnMessage()) 875 ) 876 877 else: 878 fuse_time = 3.0 879 if self.bomb_type == 'sticky': 880 sticky = True 881 model = factory.sticky_bomb_model 882 rtype = 'sharper' 883 rscale = 1.8 884 else: 885 sticky = False 886 model = factory.bomb_model 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 = ba.newnode( 896 'bomb', 897 delegate=self, 898 attrs={ 899 'position': position, 900 'velocity': velocity, 901 'model': model, 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 = ba.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 ba.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 ba.timer( 925 fuse_time, ba.WeakCall(self.handlemessage, ExplodeMessage()) 926 ) 927 928 ba.animate( 929 self.node, 930 'model_scale', 931 {0: 0, 0.2: 1.3 * self.scale, 0.26: self.scale}, 932 ) 933 934 def get_source_player( 935 self, playertype: type[PlayerType] 936 ) -> PlayerType | None: 937 """Return the source-player if one exists and is the provided type.""" 938 player: Any = self._source_player 939 return ( 940 player 941 if isinstance(player, playertype) and player.exists() 942 else None 943 ) 944 945 def on_expire(self) -> None: 946 super().on_expire() 947 948 # Release callbacks/refs so we don't wind up with dependency loops. 949 self._explode_callbacks = [] 950 951 def _handle_die(self) -> None: 952 if self.node: 953 self.node.delete() 954 955 def _handle_oob(self) -> None: 956 self.handlemessage(ba.DieMessage()) 957 958 def _handle_impact(self) -> None: 959 node = ba.getcollision().opposingnode 960 961 # If we're an impact bomb and we came from this node, don't explode. 962 # (otherwise we blow up on our own head when jumping). 963 # Alternately if we're hitting another impact-bomb from the same 964 # source, don't explode. (can cause accidental explosions if rapidly 965 # throwing/etc.) 966 node_delegate = node.getdelegate(object) 967 if node: 968 if self.bomb_type == 'impact' and ( 969 node is self.owner 970 or ( 971 isinstance(node_delegate, Bomb) 972 and node_delegate.bomb_type == 'impact' 973 and node_delegate.owner is self.owner 974 ) 975 ): 976 return 977 self.handlemessage(ExplodeMessage()) 978 979 def _handle_dropped(self) -> None: 980 if self.bomb_type == 'land_mine': 981 self.arm_timer = ba.Timer( 982 1.25, ba.WeakCall(self.handlemessage, ArmMessage()) 983 ) 984 985 # Once we've thrown a sticky bomb we can stick to it. 986 elif self.bomb_type == 'sticky': 987 988 def _setsticky(node: ba.Node) -> None: 989 if node: 990 node.stick_to_owner = True 991 992 ba.timer(0.25, lambda: _setsticky(self.node)) 993 994 def _handle_splat(self) -> None: 995 node = ba.getcollision().opposingnode 996 if ( 997 node is not self.owner 998 and ba.time() - self._last_sticky_sound_time > 1.0 999 ): 1000 self._last_sticky_sound_time = ba.time() 1001 assert self.node 1002 ba.playsound( 1003 BombFactory.get().sticky_impact_sound, 1004 2.0, 1005 position=self.node.position, 1006 ) 1007 1008 def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None: 1009 """Add a call to be run when the bomb has exploded. 1010 1011 The bomb and the new blast object are passed as arguments. 1012 """ 1013 self._explode_callbacks.append(call) 1014 1015 def explode(self) -> None: 1016 """Blows up the bomb if it has not yet done so.""" 1017 if self._exploded: 1018 return 1019 self._exploded = True 1020 if self.node: 1021 blast = Blast( 1022 position=self.node.position, 1023 velocity=self.node.velocity, 1024 blast_radius=self.blast_radius, 1025 blast_type=self.bomb_type, 1026 source_player=ba.existing(self._source_player), 1027 hit_type=self.hit_type, 1028 hit_subtype=self.hit_subtype, 1029 ).autoretain() 1030 for callback in self._explode_callbacks: 1031 callback(self, blast) 1032 1033 # We blew up so we need to go away. 1034 # NOTE TO SELF: do we actually need this delay? 1035 ba.timer(0.001, ba.WeakCall(self.handlemessage, ba.DieMessage())) 1036 1037 def _handle_warn(self) -> None: 1038 if self.texture_sequence and self.node: 1039 self.texture_sequence.rate = 30 1040 ba.playsound( 1041 BombFactory.get().warn_sound, 0.5, position=self.node.position 1042 ) 1043 1044 def _add_material(self, material: ba.Material) -> None: 1045 if not self.node: 1046 return 1047 materials = self.node.materials 1048 if material not in materials: 1049 assert isinstance(materials, tuple) 1050 self.node.materials = materials + (material,) 1051 1052 def arm(self) -> None: 1053 """Arm the bomb (for land-mines and impact-bombs). 1054 1055 These types of bombs will not explode until they have been armed. 1056 """ 1057 if not self.node: 1058 return 1059 factory = BombFactory.get() 1060 intex: Sequence[ba.Texture] 1061 if self.bomb_type == 'land_mine': 1062 intex = (factory.land_mine_lit_tex, factory.land_mine_tex) 1063 self.texture_sequence = ba.newnode( 1064 'texture_sequence', 1065 owner=self.node, 1066 attrs={'rate': 30, 'input_textures': intex}, 1067 ) 1068 ba.timer(0.5, self.texture_sequence.delete) 1069 1070 # We now make it explodable. 1071 ba.timer( 1072 0.25, 1073 ba.WeakCall( 1074 self._add_material, factory.land_mine_blast_material 1075 ), 1076 ) 1077 elif self.bomb_type == 'impact': 1078 intex = ( 1079 factory.impact_lit_tex, 1080 factory.impact_tex, 1081 factory.impact_tex, 1082 ) 1083 self.texture_sequence = ba.newnode( 1084 'texture_sequence', 1085 owner=self.node, 1086 attrs={'rate': 100, 'input_textures': intex}, 1087 ) 1088 ba.timer( 1089 0.25, 1090 ba.WeakCall( 1091 self._add_material, factory.land_mine_blast_material 1092 ), 1093 ) 1094 else: 1095 raise RuntimeError( 1096 'arm() should only be called on land-mines or impact bombs' 1097 ) 1098 self.texture_sequence.connectattr( 1099 'output_texture', self.node, 'color_texture' 1100 ) 1101 ba.playsound(factory.activate_sound, 0.5, position=self.node.position) 1102 1103 def _handle_hit(self, msg: ba.HitMessage) -> None: 1104 ispunched = msg.srcnode and msg.srcnode.getnodetype() == 'spaz' 1105 1106 # Normal bombs are triggered by non-punch impacts; 1107 # impact-bombs by all impacts. 1108 if not self._exploded and ( 1109 not ispunched or self.bomb_type in ['impact', 'land_mine'] 1110 ): 1111 1112 # Also lets change the owner of the bomb to whoever is setting 1113 # us off. (this way points for big chain reactions go to the 1114 # person causing them). 1115 source_player = msg.get_source_player(ba.Player) 1116 if source_player is not None: 1117 self._source_player = source_player 1118 1119 # Also inherit the hit type (if a landmine sets off by a bomb, 1120 # the credit should go to the mine) 1121 # the exception is TNT. TNT always gets credit. 1122 # UPDATE (July 2020): not doing this anymore. Causes too much 1123 # weird logic such as bombs acting like punches. Holler if 1124 # anything is noticeably broken due to this. 1125 # if self.bomb_type != 'tnt': 1126 # self.hit_type = msg.hit_type 1127 # self.hit_subtype = msg.hit_subtype 1128 1129 ba.timer( 1130 0.1 + random.random() * 0.1, 1131 ba.WeakCall(self.handlemessage, ExplodeMessage()), 1132 ) 1133 assert self.node 1134 self.node.handlemessage( 1135 'impulse', 1136 msg.pos[0], 1137 msg.pos[1], 1138 msg.pos[2], 1139 msg.velocity[0], 1140 msg.velocity[1], 1141 msg.velocity[2], 1142 msg.magnitude, 1143 msg.velocity_magnitude, 1144 msg.radius, 1145 0, 1146 msg.velocity[0], 1147 msg.velocity[1], 1148 msg.velocity[2], 1149 ) 1150 1151 if msg.srcnode: 1152 pass 1153 1154 def handlemessage(self, msg: Any) -> Any: 1155 if isinstance(msg, ExplodeMessage): 1156 self.explode() 1157 elif isinstance(msg, ImpactMessage): 1158 self._handle_impact() 1159 # Ok the logic below looks like it was backwards to me. 1160 # Disabling for now; can bring back if need be. 1161 # elif isinstance(msg, ba.PickedUpMessage): 1162 # # Change our source to whoever just picked us up *only* if it 1163 # # is None. This way we can get points for killing bots with their 1164 # # own bombs. Hmm would there be a downside to this? 1165 # if self._source_player is not None: 1166 # self._source_player = msg.node.source_player 1167 elif isinstance(msg, SplatMessage): 1168 self._handle_splat() 1169 elif isinstance(msg, ba.DroppedMessage): 1170 self._handle_dropped() 1171 elif isinstance(msg, ba.HitMessage): 1172 self._handle_hit(msg) 1173 elif isinstance(msg, ba.DieMessage): 1174 self._handle_die() 1175 elif isinstance(msg, ba.OutOfBoundsMessage): 1176 self._handle_oob() 1177 elif isinstance(msg, ArmMessage): 1178 self.arm() 1179 elif isinstance(msg, WarnMessage): 1180 self._handle_warn() 1181 else: 1182 super().handlemessage(msg)
A standard bomb and its variants such as land-mines and tnt-boxes.
category: Gameplay Classes
719 def __init__( 720 self, 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: ba.Player | None = None, 727 owner: ba.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: ba.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[ba.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 = ba.newnode( 815 'prop', 816 delegate=self, 817 attrs={ 818 'position': position, 819 'velocity': velocity, 820 'model': factory.land_mine_model, 821 'light_model': factory.land_mine_model, 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 = ba.newnode( 835 'prop', 836 delegate=self, 837 attrs={ 838 'position': position, 839 'velocity': velocity, 840 'model': factory.tnt_model, 841 'light_model': factory.tnt_model, 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 = ba.newnode( 855 'prop', 856 delegate=self, 857 attrs={ 858 'position': position, 859 'velocity': velocity, 860 'body': 'sphere', 861 'body_scale': self.scale, 862 'model': factory.impact_bomb_model, 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 = ba.Timer( 871 0.2, ba.WeakCall(self.handlemessage, ArmMessage()) 872 ) 873 self.warn_timer = ba.Timer( 874 fuse_time - 1.7, ba.WeakCall(self.handlemessage, WarnMessage()) 875 ) 876 877 else: 878 fuse_time = 3.0 879 if self.bomb_type == 'sticky': 880 sticky = True 881 model = factory.sticky_bomb_model 882 rtype = 'sharper' 883 rscale = 1.8 884 else: 885 sticky = False 886 model = factory.bomb_model 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 = ba.newnode( 896 'bomb', 897 delegate=self, 898 attrs={ 899 'position': position, 900 'velocity': velocity, 901 'model': model, 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 = ba.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 ba.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 ba.timer( 925 fuse_time, ba.WeakCall(self.handlemessage, ExplodeMessage()) 926 ) 927 928 ba.animate( 929 self.node, 930 'model_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( 935 self, playertype: type[PlayerType] 936 ) -> PlayerType | None: 937 """Return the source-player if one exists and is the provided type.""" 938 player: Any = self._source_player 939 return ( 940 player 941 if isinstance(player, playertype) and player.exists() 942 else None 943 )
Return the source-player if one exists and is the provided type.
945 def on_expire(self) -> None: 946 super().on_expire() 947 948 # Release callbacks/refs so we don't wind up with dependency loops. 949 self._explode_callbacks = []
Called for remaining ba.Actor
s when their ba.Activity shuts down.
Actors can use this opportunity to clear callbacks or other references which have the potential of keeping the ba.Activity alive inadvertently (Activities can not exit cleanly while any Python references to them remain.)
Once an actor is expired (see ba.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.
1008 def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None: 1009 """Add a call to be run when the bomb has exploded. 1010 1011 The bomb and the new blast object are passed as arguments. 1012 """ 1013 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.
1015 def explode(self) -> None: 1016 """Blows up the bomb if it has not yet done so.""" 1017 if self._exploded: 1018 return 1019 self._exploded = True 1020 if self.node: 1021 blast = Blast( 1022 position=self.node.position, 1023 velocity=self.node.velocity, 1024 blast_radius=self.blast_radius, 1025 blast_type=self.bomb_type, 1026 source_player=ba.existing(self._source_player), 1027 hit_type=self.hit_type, 1028 hit_subtype=self.hit_subtype, 1029 ).autoretain() 1030 for callback in self._explode_callbacks: 1031 callback(self, blast) 1032 1033 # We blew up so we need to go away. 1034 # NOTE TO SELF: do we actually need this delay? 1035 ba.timer(0.001, ba.WeakCall(self.handlemessage, ba.DieMessage()))
Blows up the bomb if it has not yet done so.
1052 def arm(self) -> None: 1053 """Arm the bomb (for land-mines and impact-bombs). 1054 1055 These types of bombs will not explode until they have been armed. 1056 """ 1057 if not self.node: 1058 return 1059 factory = BombFactory.get() 1060 intex: Sequence[ba.Texture] 1061 if self.bomb_type == 'land_mine': 1062 intex = (factory.land_mine_lit_tex, factory.land_mine_tex) 1063 self.texture_sequence = ba.newnode( 1064 'texture_sequence', 1065 owner=self.node, 1066 attrs={'rate': 30, 'input_textures': intex}, 1067 ) 1068 ba.timer(0.5, self.texture_sequence.delete) 1069 1070 # We now make it explodable. 1071 ba.timer( 1072 0.25, 1073 ba.WeakCall( 1074 self._add_material, factory.land_mine_blast_material 1075 ), 1076 ) 1077 elif self.bomb_type == 'impact': 1078 intex = ( 1079 factory.impact_lit_tex, 1080 factory.impact_tex, 1081 factory.impact_tex, 1082 ) 1083 self.texture_sequence = ba.newnode( 1084 'texture_sequence', 1085 owner=self.node, 1086 attrs={'rate': 100, 'input_textures': intex}, 1087 ) 1088 ba.timer( 1089 0.25, 1090 ba.WeakCall( 1091 self._add_material, factory.land_mine_blast_material 1092 ), 1093 ) 1094 else: 1095 raise RuntimeError( 1096 'arm() should only be called on land-mines or impact bombs' 1097 ) 1098 self.texture_sequence.connectattr( 1099 'output_texture', self.node, 'color_texture' 1100 ) 1101 ba.playsound(factory.activate_sound, 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.
1154 def handlemessage(self, msg: Any) -> Any: 1155 if isinstance(msg, ExplodeMessage): 1156 self.explode() 1157 elif isinstance(msg, ImpactMessage): 1158 self._handle_impact() 1159 # Ok the logic below looks like it was backwards to me. 1160 # Disabling for now; can bring back if need be. 1161 # elif isinstance(msg, ba.PickedUpMessage): 1162 # # Change our source to whoever just picked us up *only* if it 1163 # # is None. This way we can get points for killing bots with their 1164 # # own bombs. Hmm would there be a downside to this? 1165 # if self._source_player is not None: 1166 # self._source_player = msg.node.source_player 1167 elif isinstance(msg, SplatMessage): 1168 self._handle_splat() 1169 elif isinstance(msg, ba.DroppedMessage): 1170 self._handle_dropped() 1171 elif isinstance(msg, ba.HitMessage): 1172 self._handle_hit(msg) 1173 elif isinstance(msg, ba.DieMessage): 1174 self._handle_die() 1175 elif isinstance(msg, ba.OutOfBoundsMessage): 1176 self._handle_oob() 1177 elif isinstance(msg, ArmMessage): 1178 self.arm() 1179 elif isinstance(msg, WarnMessage): 1180 self._handle_warn() 1181 else: 1182 super().handlemessage(msg)
General message handling; can be passed any message object.
Inherited Members
- ba._actor.Actor
- autoretain
- expired
- exists
- is_alive
- activity
- getactivity
1185class TNTSpawner: 1186 """Regenerates TNT at a given point in space every now and then. 1187 1188 category: Gameplay Classes 1189 """ 1190 1191 def __init__(self, position: Sequence[float], respawn_time: float = 20.0): 1192 """Instantiate with given position and respawn_time (in seconds).""" 1193 self._position = position 1194 self._tnt: Bomb | None = None 1195 self._respawn_time = random.uniform(0.8, 1.2) * respawn_time 1196 self._wait_time = 0.0 1197 self._update() 1198 1199 # Go with slightly more than 1 second to avoid timer stacking. 1200 self._update_timer = ba.Timer( 1201 1.1, ba.WeakCall(self._update), repeat=True 1202 ) 1203 1204 def _update(self) -> None: 1205 tnt_alive = self._tnt is not None and self._tnt.node 1206 if not tnt_alive: 1207 # Respawn if its been long enough.. otherwise just increment our 1208 # how-long-since-we-died value. 1209 if self._tnt is None or self._wait_time >= self._respawn_time: 1210 self._tnt = Bomb(position=self._position, bomb_type='tnt') 1211 self._wait_time = 0.0 1212 else: 1213 self._wait_time += 1.1
Regenerates TNT at a given point in space every now and then.
category: Gameplay Classes
1191 def __init__(self, position: Sequence[float], respawn_time: float = 20.0): 1192 """Instantiate with given position and respawn_time (in seconds).""" 1193 self._position = position 1194 self._tnt: Bomb | None = None 1195 self._respawn_time = random.uniform(0.8, 1.2) * respawn_time 1196 self._wait_time = 0.0 1197 self._update() 1198 1199 # Go with slightly more than 1 second to avoid timer stacking. 1200 self._update_timer = ba.Timer( 1201 1.1, ba.WeakCall(self._update), repeat=True 1202 )
Instantiate with given position and respawn_time (in seconds).