Source code for bascenev1lib.actor.powerupbox

# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""

from __future__ import annotations

import random
from typing import TYPE_CHECKING, override

import bascenev1 as bs

from bascenev1lib.gameutils import SharedObjects

if TYPE_CHECKING:
    from typing import Any, Sequence

DEFAULT_POWERUP_INTERVAL = 8.0


class _TouchedMessage:
    pass


[docs] class PowerupBoxFactory: """A collection of media and other resources used by bs.Powerups. Category: **Gameplay Classes** A single instance of this is shared between all powerups and can be retrieved via bs.Powerup.get_factory(). """ mesh: bs.Mesh """The bs.Mesh of the powerup box.""" mesh_simple: bs.Mesh """A simpler bs.Mesh of the powerup box, for use in shadows, etc.""" tex_bomb: bs.Texture """Triple-bomb powerup bs.Texture.""" tex_punch: bs.Texture """Punch powerup bs.Texture.""" tex_ice_bombs: bs.Texture """Ice bomb powerup bs.Texture.""" tex_sticky_bombs: bs.Texture """Sticky bomb powerup bs.Texture.""" tex_shield: bs.Texture """Shield powerup bs.Texture.""" tex_impact_bombs: bs.Texture """Impact-bomb powerup bs.Texture.""" tex_health: bs.Texture """Health powerup bs.Texture.""" tex_land_mines: bs.Texture """Land-mine powerup bs.Texture.""" tex_curse: bs.Texture """Curse powerup bs.Texture.""" health_powerup_sound: bs.Sound """bs.Sound played when a health powerup is accepted.""" powerup_sound: bs.Sound """bs.Sound played when a powerup is accepted.""" powerdown_sound: bs.Sound """bs.Sound that can be used when powerups wear off.""" powerup_material: bs.Material """bs.Material applied to powerup boxes.""" powerup_accept_material: bs.Material """Powerups will send a bs.PowerupMessage to anything they touch that has this bs.Material applied.""" _STORENAME = bs.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 bascenev1 import get_default_powerup_distribution shared = SharedObjects.get() self._lastpoweruptype: str | None = None self.mesh = bs.getmesh('powerup') self.mesh_simple = bs.getmesh('powerupSimple') self.tex_bomb = bs.gettexture('powerupBomb') self.tex_punch = bs.gettexture('powerupPunch') self.tex_ice_bombs = bs.gettexture('powerupIceBombs') self.tex_sticky_bombs = bs.gettexture('powerupStickyBombs') self.tex_shield = bs.gettexture('powerupShield') self.tex_impact_bombs = bs.gettexture('powerupImpactBombs') self.tex_health = bs.gettexture('powerupHealth') self.tex_land_mines = bs.gettexture('powerupLandMines') self.tex_curse = bs.gettexture('powerupCurse') self.health_powerup_sound = bs.getsound('healthPowerup') self.powerup_sound = bs.getsound('powerup01') self.powerdown_sound = bs.getsound('powerdown01') self.drop_sound = bs.getsound('boxDrop') # Material for powerups. self.powerup_material = bs.Material() # Material for anyone wanting to accept powerups. self.powerup_accept_material = bs.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)
[docs] def get_random_powerup_type( self, forcetype: str | None = None, excludetypes: list[str] | None = None, ) -> str: """Returns a random powerup type (string). See bs.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
[docs] @classmethod def get(cls) -> PowerupBoxFactory: """Return a shared bs.PowerupBoxFactory object, creating if needed.""" activity = bs.getactivity() if activity is None: raise bs.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
[docs] class PowerupBox(bs.Actor): """A box that grants a powerup. category: Gameplay Classes This will deliver a bs.PowerupMessage to anything that touches it which has the bs.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: bs.Node """The 'prop' bs.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 bs.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 = bs.newnode( 'prop', delegate=self, attrs={ 'body': 'box', 'position': position, 'mesh': factory.mesh, 'light_mesh': factory.mesh_simple, 'shadow_size': 0.5, 'color_texture': tex, 'reflection': 'powerup', 'reflection_scale': [1.0], 'materials': (factory.powerup_material, shared.object_material), }, ) # Animate in. curve = bs.animate(self.node, 'mesh_scale', {0: 0, 0.14: 1.6, 0.2: 1}) bs.timer(0.2, curve.delete) if expire: bs.timer( DEFAULT_POWERUP_INTERVAL - 2.5, bs.WeakCall(self._start_flashing), ) bs.timer( DEFAULT_POWERUP_INTERVAL - 1.0, bs.WeakCall(self.handlemessage, bs.DieMessage()), ) def _start_flashing(self) -> None: if self.node: self.node.flashing = True
[docs] @override def handlemessage(self, msg: Any) -> Any: assert not self.expired if isinstance(msg, bs.PowerupAcceptMessage): factory = PowerupBoxFactory.get() assert self.node if self.poweruptype == 'health': factory.health_powerup_sound.play( 3, position=self.node.position ) factory.powerup_sound.play(3, position=self.node.position) self._powersgiven = True self.handlemessage(bs.DieMessage()) elif isinstance(msg, _TouchedMessage): if not self._powersgiven: node = bs.getcollision().opposingnode node.handlemessage( bs.PowerupMessage(self.poweruptype, sourcenode=self.node) ) elif isinstance(msg, bs.DieMessage): if self.node: if msg.immediate: self.node.delete() else: bs.animate(self.node, 'mesh_scale', {0: 1, 0.1: 0}) bs.timer(0.1, self.node.delete) elif isinstance(msg, bs.OutOfBoundsMessage): self.handlemessage(bs.DieMessage()) elif isinstance(msg, bs.HitMessage): # Don't die on punches (that's annoying). if msg.hit_type != 'punch': self.handlemessage(bs.DieMessage()) else: return super().handlemessage(msg) return None