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