# Released under the MIT License. See LICENSE for details. # """Defines Actor(s).""" from __future__ import annotations import random from typing import TYPE_CHECKING import ba from bastd.gameutils import SharedObjects if TYPE_CHECKING: from typing import Any, Sequence DEFAULT_POWERUP_INTERVAL = 8.0 class _TouchedMessage: pass class PowerupBoxFactory: """A collection of media and other resources used by ba.Powerups. Category: **Gameplay Classes** A single instance of this is shared between all powerups and can be retrieved via ba.Powerup.get_factory(). """ model: ba.Model """The ba.Model of the powerup box.""" model_simple: ba.Model """A simpler ba.Model of the powerup box, for use in shadows, etc.""" tex_bomb: ba.Texture """Triple-bomb powerup ba.Texture.""" tex_punch: ba.Texture """Punch powerup ba.Texture.""" tex_ice_bombs: ba.Texture """Ice bomb powerup ba.Texture.""" tex_sticky_bombs: ba.Texture """Sticky bomb powerup ba.Texture.""" tex_shield: ba.Texture """Shield powerup ba.Texture.""" tex_impact_bombs: ba.Texture """Impact-bomb powerup ba.Texture.""" tex_health: ba.Texture """Health powerup ba.Texture.""" tex_land_mines: ba.Texture """Land-mine powerup ba.Texture.""" tex_curse: ba.Texture """Curse powerup ba.Texture.""" health_powerup_sound: ba.Sound """ba.Sound played when a health powerup is accepted.""" powerup_sound: ba.Sound """ba.Sound played when a powerup is accepted.""" powerdown_sound: ba.Sound """ba.Sound that can be used when powerups wear off.""" powerup_material: ba.Material """ba.Material applied to powerup boxes.""" powerup_accept_material: ba.Material """Powerups will send a ba.PowerupMessage to anything they touch that has this ba.Material applied.""" _STORENAME = ba.storagename() def __init__(self) -> None: """Instantiate a PowerupBoxFactory. You shouldn't need to do this; call Powerup.get_factory() to get a shared instance. """ from ba.internal import get_default_powerup_distribution shared = SharedObjects.get() self._lastpoweruptype: str | None = None self.model = ba.getmodel('powerup') self.model_simple = ba.getmodel('powerupSimple') self.tex_bomb = ba.gettexture('powerupBomb') self.tex_punch = ba.gettexture('powerupPunch') self.tex_ice_bombs = ba.gettexture('powerupIceBombs') self.tex_sticky_bombs = ba.gettexture('powerupStickyBombs') self.tex_shield = ba.gettexture('powerupShield') self.tex_impact_bombs = ba.gettexture('powerupImpactBombs') self.tex_health = ba.gettexture('powerupHealth') self.tex_land_mines = ba.gettexture('powerupLandMines') self.tex_curse = ba.gettexture('powerupCurse') self.health_powerup_sound = ba.getsound('healthPowerup') self.powerup_sound = ba.getsound('powerup01') self.powerdown_sound = ba.getsound('powerdown01') self.drop_sound = ba.getsound('boxDrop') # Material for powerups. self.powerup_material = ba.Material() # Material for anyone wanting to accept powerups. self.powerup_accept_material = ba.Material() # Pass a powerup-touched message to applicable stuff. self.powerup_material.add_actions( conditions=('they_have_material', self.powerup_accept_material), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('message', 'our_node', 'at_connect', _TouchedMessage()), ), ) # We don't wanna be picked up. self.powerup_material.add_actions( conditions=('they_have_material', shared.pickup_material), actions=('modify_part_collision', 'collide', False), ) self.powerup_material.add_actions( conditions=('they_have_material', shared.footing_material), actions=('impact_sound', self.drop_sound, 0.5, 0.1), ) self._powerupdist: list[str] = [] for powerup, freq in get_default_powerup_distribution(): for _i in range(int(freq)): self._powerupdist.append(powerup) def get_random_powerup_type( self, forcetype: str | None = None, excludetypes: list[str] | None = None, ) -> str: """Returns a random powerup type (string). See ba.Powerup.poweruptype for available type values. There are certain non-random aspects to this; a 'curse' powerup, for instance, is always followed by a 'health' powerup (to keep things interesting). Passing 'forcetype' forces a given returned type while still properly interacting with the non-random aspects of the system (ie: forcing a 'curse' powerup will result in the next powerup being health). """ if excludetypes is None: excludetypes = [] if forcetype: ptype = forcetype else: # If the last one was a curse, make this one a health to # provide some hope. if self._lastpoweruptype == 'curse': ptype = 'health' else: while True: ptype = self._powerupdist[ random.randint(0, len(self._powerupdist) - 1) ] if ptype not in excludetypes: break self._lastpoweruptype = ptype return ptype @classmethod def get(cls) -> PowerupBoxFactory: """Return a shared ba.PowerupBoxFactory object, creating if needed.""" activity = ba.getactivity() if activity is None: raise ba.ContextError('No current activity.') factory = activity.customdata.get(cls._STORENAME) if factory is None: factory = activity.customdata[cls._STORENAME] = PowerupBoxFactory() assert isinstance(factory, PowerupBoxFactory) return factory class PowerupBox(ba.Actor): """A box that grants a powerup. category: Gameplay Classes This will deliver a ba.PowerupMessage to anything that touches it which has the ba.PowerupBoxFactory.powerup_accept_material applied. """ poweruptype: str """The string powerup type. This can be 'triple_bombs', 'punch', 'ice_bombs', 'impact_bombs', 'land_mines', 'sticky_bombs', 'shield', 'health', or 'curse'.""" node: ba.Node """The 'prop' ba.Node representing this box.""" def __init__( self, position: Sequence[float] = (0.0, 1.0, 0.0), poweruptype: str = 'triple_bombs', expire: bool = True, ): """Create a powerup-box of the requested type at the given position. see ba.Powerup.poweruptype for valid type strings. """ super().__init__() shared = SharedObjects.get() factory = PowerupBoxFactory.get() self.poweruptype = poweruptype self._powersgiven = False if poweruptype == 'triple_bombs': tex = factory.tex_bomb elif poweruptype == 'punch': tex = factory.tex_punch elif poweruptype == 'ice_bombs': tex = factory.tex_ice_bombs elif poweruptype == 'impact_bombs': tex = factory.tex_impact_bombs elif poweruptype == 'land_mines': tex = factory.tex_land_mines elif poweruptype == 'sticky_bombs': tex = factory.tex_sticky_bombs elif poweruptype == 'shield': tex = factory.tex_shield elif poweruptype == 'health': tex = factory.tex_health elif poweruptype == 'curse': tex = factory.tex_curse else: raise ValueError('invalid poweruptype: ' + str(poweruptype)) if len(position) != 3: raise ValueError('expected 3 floats for position') self.node = ba.newnode( 'prop', delegate=self, attrs={ 'body': 'box', 'position': position, 'model': factory.model, 'light_model': factory.model_simple, 'shadow_size': 0.5, 'color_texture': tex, 'reflection': 'powerup', 'reflection_scale': [1.0], 'materials': (factory.powerup_material, shared.object_material), }, ) # yapf: disable # Animate in. curve = ba.animate(self.node, 'model_scale', {0: 0, 0.14: 1.6, 0.2: 1}) ba.timer(0.2, curve.delete) if expire: ba.timer( DEFAULT_POWERUP_INTERVAL - 2.5, ba.WeakCall(self._start_flashing), ) ba.timer( DEFAULT_POWERUP_INTERVAL - 1.0, ba.WeakCall(self.handlemessage, ba.DieMessage()), ) def _start_flashing(self) -> None: if self.node: self.node.flashing = True def handlemessage(self, msg: Any) -> Any: assert not self.expired if isinstance(msg, ba.PowerupAcceptMessage): factory = PowerupBoxFactory.get() assert self.node if self.poweruptype == 'health': ba.playsound( factory.health_powerup_sound, 3, position=self.node.position ) ba.playsound(factory.powerup_sound, 3, position=self.node.position) self._powersgiven = True self.handlemessage(ba.DieMessage()) elif isinstance(msg, _TouchedMessage): if not self._powersgiven: node = ba.getcollision().opposingnode node.handlemessage( ba.PowerupMessage(self.poweruptype, sourcenode=self.node) ) elif isinstance(msg, ba.DieMessage): if self.node: if msg.immediate: self.node.delete() else: ba.animate(self.node, 'model_scale', {0: 1, 0.1: 0}) ba.timer(0.1, self.node.delete) elif isinstance(msg, ba.OutOfBoundsMessage): self.handlemessage(ba.DieMessage()) elif isinstance(msg, ba.HitMessage): # Don't die on punches (that's annoying). if msg.hit_type != 'punch': self.handlemessage(ba.DieMessage()) else: return super().handlemessage(msg) return None