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