# 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.
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.
This will deliver a :class:`~bascenev1.PowerupMessage` to anything
that touches it which has the
:class:`~PowerupBoxFactory.powerup_accept_material` applied.
"""
#: The string powerup type. This can be 'triple_bombs', 'punch',
#: 'ice_bombs', 'impact_bombs', 'land_mines', 'sticky_bombs',
#: 'shield', 'health', or 'curse'.
poweruptype: str
node: bs.Node
"""The 'prop' 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
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum