# Released under the MIT License. See LICENSE for details.
#
"""Provides a factory object from creating Spazzes."""
from __future__ import annotations
from typing import TYPE_CHECKING
import bascenev1 as bs
from bascenev1lib.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
[docs]
class SpazFactory:
"""Wraps up media and other resources used by bs.Spaz instances.
Generally one of these is created per bascenev1.Activity and shared
between all spaz instances. Use bs.Spaz.get_factory() to return
the shared factory for the current activity.
"""
impact_sounds_medium: Sequence[bs.Sound]
"""A tuple of bs.Sound-s for when a bs.Spaz hits something kinda hard."""
impact_sounds_hard: Sequence[bs.Sound]
"""A tuple of bs.Sound-s for when a bs.Spaz hits something really hard."""
impact_sounds_harder: Sequence[bs.Sound]
"""A tuple of bs.Sound-s for when a bs.Spaz hits something really
really hard."""
single_player_death_sound: bs.Sound
"""The sound that plays for an 'important' spaz death such as in
co-op games."""
punch_sound_weak: bs.Sound
"""A weak punch bs.Sound."""
punch_sound: bs.Sound
"""A standard punch bs.Sound."""
punch_sound_strong: Sequence[bs.Sound]
"""A tuple of stronger sounding punch bs.Sounds."""
punch_sound_stronger: bs.Sound
"""A really really strong sounding punch bs.Sound."""
swish_sound: bs.Sound
"""A punch swish bs.Sound."""
block_sound: bs.Sound
"""A bs.Sound for when an attack is blocked by invincibility."""
shatter_sound: bs.Sound
"""A bs.Sound for when a frozen bs.Spaz shatters."""
splatter_sound: bs.Sound
"""A bs.Sound for when a bs.Spaz blows up via curse."""
spaz_material: bs.Material
"""A bs.Material applied to all of parts of a bs.Spaz."""
roller_material: bs.Material
"""A bs.Material applied to the invisible roller ball body that
a bs.Spaz uses for locomotion."""
punch_material: bs.Material
"""A bs.Material applied to the 'fist' of a bs.Spaz."""
pickup_material: bs.Material
"""A bs.Material applied to the 'grabber' body of a bs.Spaz."""
curse_material: bs.Material
"""A bs.Material applied to a cursed bs.Spaz that triggers an explosion."""
_STORENAME = bs.storagename()
def _preload(self, character: str) -> None:
"""Preload media needed for a given character."""
self.get_media(character)
def __init__(self) -> None:
"""Instantiate a factory object."""
# pylint: disable=cyclic-import
plus = bs.app.plus
assert plus is not None
# FIXME: should probably put these somewhere common so we don't
# have to import them from a module that imports us.
from bascenev1lib.actor.spaz import (
PickupMessage,
PunchHitMessage,
CurseExplodeMessage,
)
shared = SharedObjects.get()
self.impact_sounds_medium = (
bs.getsound('impactMedium'),
bs.getsound('impactMedium2'),
)
self.impact_sounds_hard = (
bs.getsound('impactHard'),
bs.getsound('impactHard2'),
bs.getsound('impactHard3'),
)
self.impact_sounds_harder = (
bs.getsound('bigImpact'),
bs.getsound('bigImpact2'),
)
self.single_player_death_sound = bs.getsound('playerDeath')
self.punch_sound_weak = bs.getsound('punchWeak01')
self.punch_sound = bs.getsound('punch01')
self.punch_sound_strong = (
bs.getsound('punchStrong01'),
bs.getsound('punchStrong02'),
)
self.punch_sound_stronger = bs.getsound('superPunch')
self.swish_sound = bs.getsound('punchSwish')
self.block_sound = bs.getsound('block')
self.shatter_sound = bs.getsound('shatter')
self.splatter_sound = bs.getsound('splatter')
self.spaz_material = bs.Material()
self.roller_material = bs.Material()
self.punch_material = bs.Material()
self.pickup_material = bs.Material()
self.curse_material = bs.Material()
footing_material = shared.footing_material
object_material = shared.object_material
player_material = shared.player_material
region_material = shared.region_material
# Send footing messages to spazzes so they know when they're on
# solid ground.
# Eww; this probably should just be built into the spaz node.
self.roller_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1),
),
)
self.spaz_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1),
),
)
# Punches.
self.punch_material.add_actions(
conditions=('they_are_different_node_than_us',),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', PunchHitMessage()),
),
)
# Pickups.
self.pickup_material.add_actions(
conditions=(
('they_are_different_node_than_us',),
'and',
('they_have_material', object_material),
),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', PickupMessage()),
),
)
# Curse.
self.curse_material.add_actions(
conditions=(
('they_are_different_node_than_us',),
'and',
('they_have_material', player_material),
),
actions=(
'message',
'our_node',
'at_connect',
CurseExplodeMessage(),
),
)
self.foot_impact_sounds = (
bs.getsound('footImpact01'),
bs.getsound('footImpact02'),
bs.getsound('footImpact03'),
)
self.foot_skid_sound = bs.getsound('skid01')
self.foot_roll_sound = bs.getsound('scamper01')
self.roller_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('impact_sound', self.foot_impact_sounds, 1, 0.2),
('skid_sound', self.foot_skid_sound, 20, 0.3),
('roll_sound', self.foot_roll_sound, 20, 3.0),
),
)
self.skid_sound = bs.getsound('gravelSkid')
self.spaz_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('impact_sound', self.foot_impact_sounds, 20, 6),
('skid_sound', self.skid_sound, 2.0, 1),
('roll_sound', self.skid_sound, 2.0, 1),
),
)
self.shield_up_sound = bs.getsound('shieldUp')
self.shield_down_sound = bs.getsound('shieldDown')
self.shield_hit_sound = bs.getsound('shieldHit')
# We don't want to collide with stuff we're initially overlapping
# (unless its marked with a special region material).
self.spaz_material.add_actions(
conditions=(
(
('we_are_younger_than', 51),
'and',
('they_are_different_node_than_us',),
),
'and',
('they_dont_have_material', region_material),
),
actions=('modify_node_collision', 'collide', False),
)
self.spaz_media: dict[str, Any] = {}
# Lets load some basic rules.
# (allows them to be tweaked from the master server)
self.shield_decay_rate = plus.get_v1_account_misc_read_val('rsdr', 10.0)
self.punch_cooldown = plus.get_v1_account_misc_read_val('rpc', 400)
self.punch_cooldown_gloves = plus.get_v1_account_misc_read_val(
'rpcg', 300
)
self.punch_power_scale = plus.get_v1_account_misc_read_val('rpp', 1.2)
self.punch_power_scale_gloves = plus.get_v1_account_misc_read_val(
'rppg', 1.4
)
self.max_shield_spillover_damage = plus.get_v1_account_misc_read_val(
'rsms', 500
)
[docs]
def get_style(self, character: str) -> str:
"""Return the named style for this character.
(this influences subtle aspects of their appearance, etc)
"""
assert bs.app.classic is not None
return bs.app.classic.spaz_appearances[character].style
[docs]
@classmethod
def get(cls) -> SpazFactory:
"""Return the shared bs.SpazFactory, creating it if necessary."""
# pylint: disable=cyclic-import
activity = bs.getactivity()
factory = activity.customdata.get(cls._STORENAME)
if factory is None:
factory = activity.customdata[cls._STORENAME] = SpazFactory()
assert isinstance(factory, SpazFactory)
return factory
# 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