bascenev1lib.actor.playerspaz

Functionality related to player-controlled Spazzes.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Functionality related to player-controlled Spazzes."""
  4
  5from __future__ import annotations
  6
  7from typing import TYPE_CHECKING, TypeVar, overload
  8
  9from typing_extensions import override
 10import bascenev1 as bs
 11
 12from bascenev1lib.actor.spaz import Spaz
 13
 14if TYPE_CHECKING:
 15    from typing import Any, Sequence, Literal
 16
 17PlayerT = TypeVar('PlayerT', bound=bs.Player)
 18
 19
 20class PlayerSpazHurtMessage:
 21    """A message saying a PlayerSpaz was hurt.
 22
 23    Category: **Message Classes**
 24    """
 25
 26    spaz: PlayerSpaz
 27    """The PlayerSpaz that was hurt"""
 28
 29    def __init__(self, spaz: PlayerSpaz):
 30        """Instantiate with the given bascenev1.Spaz value."""
 31        self.spaz = spaz
 32
 33
 34class PlayerSpaz(Spaz):
 35    """A Spaz subclass meant to be controlled by a bascenev1.Player.
 36
 37    Category: **Gameplay Classes**
 38
 39    When a PlayerSpaz dies, it delivers a bascenev1.PlayerDiedMessage
 40    to the current bascenev1.Activity. (unless the death was the result
 41    of the player leaving the game, in which case no message is sent)
 42
 43    When a PlayerSpaz is hurt, it delivers a PlayerSpazHurtMessage
 44    to the current bascenev1.Activity.
 45    """
 46
 47    def __init__(
 48        self,
 49        player: bs.Player,
 50        color: Sequence[float] = (1.0, 1.0, 1.0),
 51        highlight: Sequence[float] = (0.5, 0.5, 0.5),
 52        character: str = 'Spaz',
 53        powerups_expire: bool = True,
 54    ):
 55        """Create a spaz for the provided bascenev1.Player.
 56
 57        Note: this does not wire up any controls;
 58        you must call connect_controls_to_player() to do so.
 59        """
 60
 61        super().__init__(
 62            color=color,
 63            highlight=highlight,
 64            character=character,
 65            source_player=player,
 66            start_invincible=True,
 67            powerups_expire=powerups_expire,
 68        )
 69        self.last_player_attacked_by: bs.Player | None = None
 70        self.last_attacked_time = 0.0
 71        self.last_attacked_type: tuple[str, str] | None = None
 72        self.held_count = 0
 73        self.last_player_held_by: bs.Player | None = None
 74        self._player = player
 75        self._drive_player_position()
 76
 77    # Overloads to tell the type system our return type based on doraise val.
 78
 79    @overload
 80    def getplayer(
 81        self, playertype: type[PlayerT], doraise: Literal[False] = False
 82    ) -> PlayerT | None:
 83        ...
 84
 85    @overload
 86    def getplayer(
 87        self, playertype: type[PlayerT], doraise: Literal[True]
 88    ) -> PlayerT:
 89        ...
 90
 91    def getplayer(
 92        self, playertype: type[PlayerT], doraise: bool = False
 93    ) -> PlayerT | None:
 94        """Get the bascenev1.Player associated with this Spaz.
 95
 96        By default this will return None if the Player no longer exists.
 97        If you are logically certain that the Player still exists, pass
 98        doraise=False to get a non-optional return type.
 99        """
100        player: Any = self._player
101        assert isinstance(player, playertype)
102        if not player.exists() and doraise:
103            raise bs.PlayerNotFoundError()
104        return player if player.exists() else None
105
106    def connect_controls_to_player(
107        self,
108        enable_jump: bool = True,
109        enable_punch: bool = True,
110        enable_pickup: bool = True,
111        enable_bomb: bool = True,
112        enable_run: bool = True,
113        enable_fly: bool = True,
114    ) -> None:
115        """Wire this spaz up to the provided bascenev1.Player.
116
117        Full control of the character is given by default
118        but can be selectively limited by passing False
119        to specific arguments.
120        """
121        player = self.getplayer(bs.Player)
122        assert player
123
124        # Reset any currently connected player and/or the player we're
125        # wiring up.
126        if self._connected_to_player:
127            if player != self._connected_to_player:
128                player.resetinput()
129            self.disconnect_controls_from_player()
130        else:
131            player.resetinput()
132
133        player.assigninput(bs.InputType.UP_DOWN, self.on_move_up_down)
134        player.assigninput(bs.InputType.LEFT_RIGHT, self.on_move_left_right)
135        player.assigninput(
136            bs.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press
137        )
138        player.assigninput(
139            bs.InputType.HOLD_POSITION_RELEASE,
140            self.on_hold_position_release,
141        )
142        intp = bs.InputType
143        if enable_jump:
144            player.assigninput(intp.JUMP_PRESS, self.on_jump_press)
145            player.assigninput(intp.JUMP_RELEASE, self.on_jump_release)
146        if enable_pickup:
147            player.assigninput(intp.PICK_UP_PRESS, self.on_pickup_press)
148            player.assigninput(intp.PICK_UP_RELEASE, self.on_pickup_release)
149        if enable_punch:
150            player.assigninput(intp.PUNCH_PRESS, self.on_punch_press)
151            player.assigninput(intp.PUNCH_RELEASE, self.on_punch_release)
152        if enable_bomb:
153            player.assigninput(intp.BOMB_PRESS, self.on_bomb_press)
154            player.assigninput(intp.BOMB_RELEASE, self.on_bomb_release)
155        if enable_run:
156            player.assigninput(intp.RUN, self.on_run)
157        if enable_fly:
158            player.assigninput(intp.FLY_PRESS, self.on_fly_press)
159            player.assigninput(intp.FLY_RELEASE, self.on_fly_release)
160
161        self._connected_to_player = player
162
163    def disconnect_controls_from_player(self) -> None:
164        """
165        Completely sever any previously connected
166        bascenev1.Player from control of this spaz.
167        """
168        if self._connected_to_player:
169            self._connected_to_player.resetinput()
170            self._connected_to_player = None
171
172            # Send releases for anything in case its held.
173            self.on_move_up_down(0)
174            self.on_move_left_right(0)
175            self.on_hold_position_release()
176            self.on_jump_release()
177            self.on_pickup_release()
178            self.on_punch_release()
179            self.on_bomb_release()
180            self.on_run(0.0)
181            self.on_fly_release()
182        else:
183            print(
184                'WARNING: disconnect_controls_from_player() called for'
185                ' non-connected player'
186            )
187
188    @override
189    def handlemessage(self, msg: Any) -> Any:
190        # FIXME: Tidy this up.
191        # pylint: disable=too-many-branches
192        # pylint: disable=too-many-statements
193        # pylint: disable=too-many-nested-blocks
194        assert not self.expired
195
196        # Keep track of if we're being held and by who most recently.
197        if isinstance(msg, bs.PickedUpMessage):
198            # Augment standard behavior.
199            super().handlemessage(msg)
200            self.held_count += 1
201            picked_up_by = msg.node.source_player
202            if picked_up_by:
203                self.last_player_held_by = picked_up_by
204        elif isinstance(msg, bs.DroppedMessage):
205            # Augment standard behavior.
206            super().handlemessage(msg)
207            self.held_count -= 1
208            if self.held_count < 0:
209                print('ERROR: spaz held_count < 0')
210
211            # Let's count someone dropping us as an attack.
212            picked_up_by = msg.node.source_player
213            if picked_up_by:
214                self.last_player_attacked_by = picked_up_by
215                self.last_attacked_time = bs.time()
216                self.last_attacked_type = ('picked_up', 'default')
217        elif isinstance(msg, bs.StandMessage):
218            super().handlemessage(msg)  # Augment standard behavior.
219
220            # Our Spaz was just moved somewhere. Explicitly update
221            # our associated player's position in case it is being used
222            # for logic (otherwise it will be out of date until next step)
223            self._drive_player_position()
224
225        elif isinstance(msg, bs.DieMessage):
226            # Report player deaths to the game.
227            if not self._dead:
228                # Immediate-mode or left-game deaths don't count as 'kills'.
229                killed = (
230                    not msg.immediate and msg.how is not bs.DeathType.LEFT_GAME
231                )
232
233                activity = self._activity()
234
235                player = self.getplayer(bs.Player, False)
236                if not killed:
237                    killerplayer = None
238                else:
239                    # If this player was being held at the time of death,
240                    # the holder is the killer.
241                    if self.held_count > 0 and self.last_player_held_by:
242                        killerplayer = self.last_player_held_by
243                    else:
244                        # Otherwise, if they were attacked by someone in the
245                        # last few seconds, that person is the killer.
246                        # Otherwise it was a suicide.
247                        # FIXME: Currently disabling suicides in Co-Op since
248                        #  all bot kills would register as suicides; need to
249                        #  change this from last_player_attacked_by to
250                        #  something like last_actor_attacked_by to fix that.
251                        if (
252                            self.last_player_attacked_by
253                            and bs.time() - self.last_attacked_time < 4.0
254                        ):
255                            killerplayer = self.last_player_attacked_by
256                        else:
257                            # ok, call it a suicide unless we're in co-op
258                            if activity is not None and not isinstance(
259                                activity.session, bs.CoopSession
260                            ):
261                                killerplayer = player
262                            else:
263                                killerplayer = None
264
265                # We should never wind up with a dead-reference here;
266                # we want to use None in that case.
267                assert killerplayer is None or killerplayer
268
269                # Only report if both the player and the activity still exist.
270                if killed and activity is not None and player:
271                    activity.handlemessage(
272                        bs.PlayerDiedMessage(
273                            player, killed, killerplayer, msg.how
274                        )
275                    )
276
277            super().handlemessage(msg)  # Augment standard behavior.
278
279        # Keep track of the player who last hit us for point rewarding.
280        elif isinstance(msg, bs.HitMessage):
281            source_player = msg.get_source_player(type(self._player))
282            if source_player:
283                self.last_player_attacked_by = source_player
284                self.last_attacked_time = bs.time()
285                self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
286            super().handlemessage(msg)  # Augment standard behavior.
287            activity = self._activity()
288            if activity is not None and self._player.exists():
289                activity.handlemessage(PlayerSpazHurtMessage(self))
290        else:
291            return super().handlemessage(msg)
292        return None
293
294    def _drive_player_position(self) -> None:
295        """Drive our bascenev1.Player's official position
296
297        If our position is changed explicitly, this should be called again
298        to instantly update the player position (otherwise it would be out
299        of date until the next sim step)
300        """
301        player = self._player
302        if player:
303            assert self.node
304            assert player.node
305            self.node.connectattr('torso_position', player.node, 'position')
class PlayerSpazHurtMessage:
21class PlayerSpazHurtMessage:
22    """A message saying a PlayerSpaz was hurt.
23
24    Category: **Message Classes**
25    """
26
27    spaz: PlayerSpaz
28    """The PlayerSpaz that was hurt"""
29
30    def __init__(self, spaz: PlayerSpaz):
31        """Instantiate with the given bascenev1.Spaz value."""
32        self.spaz = spaz

A message saying a PlayerSpaz was hurt.

Category: Message Classes

PlayerSpazHurtMessage(spaz: PlayerSpaz)
30    def __init__(self, spaz: PlayerSpaz):
31        """Instantiate with the given bascenev1.Spaz value."""
32        self.spaz = spaz

Instantiate with the given bascenev1.Spaz value.

spaz: PlayerSpaz

The PlayerSpaz that was hurt

class PlayerSpaz(bascenev1lib.actor.spaz.Spaz):
 35class PlayerSpaz(Spaz):
 36    """A Spaz subclass meant to be controlled by a bascenev1.Player.
 37
 38    Category: **Gameplay Classes**
 39
 40    When a PlayerSpaz dies, it delivers a bascenev1.PlayerDiedMessage
 41    to the current bascenev1.Activity. (unless the death was the result
 42    of the player leaving the game, in which case no message is sent)
 43
 44    When a PlayerSpaz is hurt, it delivers a PlayerSpazHurtMessage
 45    to the current bascenev1.Activity.
 46    """
 47
 48    def __init__(
 49        self,
 50        player: bs.Player,
 51        color: Sequence[float] = (1.0, 1.0, 1.0),
 52        highlight: Sequence[float] = (0.5, 0.5, 0.5),
 53        character: str = 'Spaz',
 54        powerups_expire: bool = True,
 55    ):
 56        """Create a spaz for the provided bascenev1.Player.
 57
 58        Note: this does not wire up any controls;
 59        you must call connect_controls_to_player() to do so.
 60        """
 61
 62        super().__init__(
 63            color=color,
 64            highlight=highlight,
 65            character=character,
 66            source_player=player,
 67            start_invincible=True,
 68            powerups_expire=powerups_expire,
 69        )
 70        self.last_player_attacked_by: bs.Player | None = None
 71        self.last_attacked_time = 0.0
 72        self.last_attacked_type: tuple[str, str] | None = None
 73        self.held_count = 0
 74        self.last_player_held_by: bs.Player | None = None
 75        self._player = player
 76        self._drive_player_position()
 77
 78    # Overloads to tell the type system our return type based on doraise val.
 79
 80    @overload
 81    def getplayer(
 82        self, playertype: type[PlayerT], doraise: Literal[False] = False
 83    ) -> PlayerT | None:
 84        ...
 85
 86    @overload
 87    def getplayer(
 88        self, playertype: type[PlayerT], doraise: Literal[True]
 89    ) -> PlayerT:
 90        ...
 91
 92    def getplayer(
 93        self, playertype: type[PlayerT], doraise: bool = False
 94    ) -> PlayerT | None:
 95        """Get the bascenev1.Player associated with this Spaz.
 96
 97        By default this will return None if the Player no longer exists.
 98        If you are logically certain that the Player still exists, pass
 99        doraise=False to get a non-optional return type.
100        """
101        player: Any = self._player
102        assert isinstance(player, playertype)
103        if not player.exists() and doraise:
104            raise bs.PlayerNotFoundError()
105        return player if player.exists() else None
106
107    def connect_controls_to_player(
108        self,
109        enable_jump: bool = True,
110        enable_punch: bool = True,
111        enable_pickup: bool = True,
112        enable_bomb: bool = True,
113        enable_run: bool = True,
114        enable_fly: bool = True,
115    ) -> None:
116        """Wire this spaz up to the provided bascenev1.Player.
117
118        Full control of the character is given by default
119        but can be selectively limited by passing False
120        to specific arguments.
121        """
122        player = self.getplayer(bs.Player)
123        assert player
124
125        # Reset any currently connected player and/or the player we're
126        # wiring up.
127        if self._connected_to_player:
128            if player != self._connected_to_player:
129                player.resetinput()
130            self.disconnect_controls_from_player()
131        else:
132            player.resetinput()
133
134        player.assigninput(bs.InputType.UP_DOWN, self.on_move_up_down)
135        player.assigninput(bs.InputType.LEFT_RIGHT, self.on_move_left_right)
136        player.assigninput(
137            bs.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press
138        )
139        player.assigninput(
140            bs.InputType.HOLD_POSITION_RELEASE,
141            self.on_hold_position_release,
142        )
143        intp = bs.InputType
144        if enable_jump:
145            player.assigninput(intp.JUMP_PRESS, self.on_jump_press)
146            player.assigninput(intp.JUMP_RELEASE, self.on_jump_release)
147        if enable_pickup:
148            player.assigninput(intp.PICK_UP_PRESS, self.on_pickup_press)
149            player.assigninput(intp.PICK_UP_RELEASE, self.on_pickup_release)
150        if enable_punch:
151            player.assigninput(intp.PUNCH_PRESS, self.on_punch_press)
152            player.assigninput(intp.PUNCH_RELEASE, self.on_punch_release)
153        if enable_bomb:
154            player.assigninput(intp.BOMB_PRESS, self.on_bomb_press)
155            player.assigninput(intp.BOMB_RELEASE, self.on_bomb_release)
156        if enable_run:
157            player.assigninput(intp.RUN, self.on_run)
158        if enable_fly:
159            player.assigninput(intp.FLY_PRESS, self.on_fly_press)
160            player.assigninput(intp.FLY_RELEASE, self.on_fly_release)
161
162        self._connected_to_player = player
163
164    def disconnect_controls_from_player(self) -> None:
165        """
166        Completely sever any previously connected
167        bascenev1.Player from control of this spaz.
168        """
169        if self._connected_to_player:
170            self._connected_to_player.resetinput()
171            self._connected_to_player = None
172
173            # Send releases for anything in case its held.
174            self.on_move_up_down(0)
175            self.on_move_left_right(0)
176            self.on_hold_position_release()
177            self.on_jump_release()
178            self.on_pickup_release()
179            self.on_punch_release()
180            self.on_bomb_release()
181            self.on_run(0.0)
182            self.on_fly_release()
183        else:
184            print(
185                'WARNING: disconnect_controls_from_player() called for'
186                ' non-connected player'
187            )
188
189    @override
190    def handlemessage(self, msg: Any) -> Any:
191        # FIXME: Tidy this up.
192        # pylint: disable=too-many-branches
193        # pylint: disable=too-many-statements
194        # pylint: disable=too-many-nested-blocks
195        assert not self.expired
196
197        # Keep track of if we're being held and by who most recently.
198        if isinstance(msg, bs.PickedUpMessage):
199            # Augment standard behavior.
200            super().handlemessage(msg)
201            self.held_count += 1
202            picked_up_by = msg.node.source_player
203            if picked_up_by:
204                self.last_player_held_by = picked_up_by
205        elif isinstance(msg, bs.DroppedMessage):
206            # Augment standard behavior.
207            super().handlemessage(msg)
208            self.held_count -= 1
209            if self.held_count < 0:
210                print('ERROR: spaz held_count < 0')
211
212            # Let's count someone dropping us as an attack.
213            picked_up_by = msg.node.source_player
214            if picked_up_by:
215                self.last_player_attacked_by = picked_up_by
216                self.last_attacked_time = bs.time()
217                self.last_attacked_type = ('picked_up', 'default')
218        elif isinstance(msg, bs.StandMessage):
219            super().handlemessage(msg)  # Augment standard behavior.
220
221            # Our Spaz was just moved somewhere. Explicitly update
222            # our associated player's position in case it is being used
223            # for logic (otherwise it will be out of date until next step)
224            self._drive_player_position()
225
226        elif isinstance(msg, bs.DieMessage):
227            # Report player deaths to the game.
228            if not self._dead:
229                # Immediate-mode or left-game deaths don't count as 'kills'.
230                killed = (
231                    not msg.immediate and msg.how is not bs.DeathType.LEFT_GAME
232                )
233
234                activity = self._activity()
235
236                player = self.getplayer(bs.Player, False)
237                if not killed:
238                    killerplayer = None
239                else:
240                    # If this player was being held at the time of death,
241                    # the holder is the killer.
242                    if self.held_count > 0 and self.last_player_held_by:
243                        killerplayer = self.last_player_held_by
244                    else:
245                        # Otherwise, if they were attacked by someone in the
246                        # last few seconds, that person is the killer.
247                        # Otherwise it was a suicide.
248                        # FIXME: Currently disabling suicides in Co-Op since
249                        #  all bot kills would register as suicides; need to
250                        #  change this from last_player_attacked_by to
251                        #  something like last_actor_attacked_by to fix that.
252                        if (
253                            self.last_player_attacked_by
254                            and bs.time() - self.last_attacked_time < 4.0
255                        ):
256                            killerplayer = self.last_player_attacked_by
257                        else:
258                            # ok, call it a suicide unless we're in co-op
259                            if activity is not None and not isinstance(
260                                activity.session, bs.CoopSession
261                            ):
262                                killerplayer = player
263                            else:
264                                killerplayer = None
265
266                # We should never wind up with a dead-reference here;
267                # we want to use None in that case.
268                assert killerplayer is None or killerplayer
269
270                # Only report if both the player and the activity still exist.
271                if killed and activity is not None and player:
272                    activity.handlemessage(
273                        bs.PlayerDiedMessage(
274                            player, killed, killerplayer, msg.how
275                        )
276                    )
277
278            super().handlemessage(msg)  # Augment standard behavior.
279
280        # Keep track of the player who last hit us for point rewarding.
281        elif isinstance(msg, bs.HitMessage):
282            source_player = msg.get_source_player(type(self._player))
283            if source_player:
284                self.last_player_attacked_by = source_player
285                self.last_attacked_time = bs.time()
286                self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
287            super().handlemessage(msg)  # Augment standard behavior.
288            activity = self._activity()
289            if activity is not None and self._player.exists():
290                activity.handlemessage(PlayerSpazHurtMessage(self))
291        else:
292            return super().handlemessage(msg)
293        return None
294
295    def _drive_player_position(self) -> None:
296        """Drive our bascenev1.Player's official position
297
298        If our position is changed explicitly, this should be called again
299        to instantly update the player position (otherwise it would be out
300        of date until the next sim step)
301        """
302        player = self._player
303        if player:
304            assert self.node
305            assert player.node
306            self.node.connectattr('torso_position', player.node, 'position')

A Spaz subclass meant to be controlled by a bascenev1.Player.

Category: Gameplay Classes

When a PlayerSpaz dies, it delivers a bascenev1.PlayerDiedMessage to the current bascenev1.Activity. (unless the death was the result of the player leaving the game, in which case no message is sent)

When a PlayerSpaz is hurt, it delivers a PlayerSpazHurtMessage to the current bascenev1.Activity.

PlayerSpaz( player: bascenev1._player.Player, color: Sequence[float] = (1.0, 1.0, 1.0), highlight: Sequence[float] = (0.5, 0.5, 0.5), character: str = 'Spaz', powerups_expire: bool = True)
48    def __init__(
49        self,
50        player: bs.Player,
51        color: Sequence[float] = (1.0, 1.0, 1.0),
52        highlight: Sequence[float] = (0.5, 0.5, 0.5),
53        character: str = 'Spaz',
54        powerups_expire: bool = True,
55    ):
56        """Create a spaz for the provided bascenev1.Player.
57
58        Note: this does not wire up any controls;
59        you must call connect_controls_to_player() to do so.
60        """
61
62        super().__init__(
63            color=color,
64            highlight=highlight,
65            character=character,
66            source_player=player,
67            start_invincible=True,
68            powerups_expire=powerups_expire,
69        )
70        self.last_player_attacked_by: bs.Player | None = None
71        self.last_attacked_time = 0.0
72        self.last_attacked_type: tuple[str, str] | None = None
73        self.held_count = 0
74        self.last_player_held_by: bs.Player | None = None
75        self._player = player
76        self._drive_player_position()

Create a spaz for the provided bascenev1.Player.

Note: this does not wire up any controls; you must call connect_controls_to_player() to do so.

last_player_attacked_by: bascenev1._player.Player | None
last_attacked_time
last_attacked_type: tuple[str, str] | None
held_count
last_player_held_by: bascenev1._player.Player | None
def getplayer( self, playertype: type[~PlayerT], doraise: bool = False) -> Optional[~PlayerT]:
 92    def getplayer(
 93        self, playertype: type[PlayerT], doraise: bool = False
 94    ) -> PlayerT | None:
 95        """Get the bascenev1.Player associated with this Spaz.
 96
 97        By default this will return None if the Player no longer exists.
 98        If you are logically certain that the Player still exists, pass
 99        doraise=False to get a non-optional return type.
100        """
101        player: Any = self._player
102        assert isinstance(player, playertype)
103        if not player.exists() and doraise:
104            raise bs.PlayerNotFoundError()
105        return player if player.exists() else None

Get the bascenev1.Player associated with this Spaz.

By default this will return None if the Player no longer exists. If you are logically certain that the Player still exists, pass doraise=False to get a non-optional return type.

def connect_controls_to_player( self, enable_jump: bool = True, enable_punch: bool = True, enable_pickup: bool = True, enable_bomb: bool = True, enable_run: bool = True, enable_fly: bool = True) -> None:
107    def connect_controls_to_player(
108        self,
109        enable_jump: bool = True,
110        enable_punch: bool = True,
111        enable_pickup: bool = True,
112        enable_bomb: bool = True,
113        enable_run: bool = True,
114        enable_fly: bool = True,
115    ) -> None:
116        """Wire this spaz up to the provided bascenev1.Player.
117
118        Full control of the character is given by default
119        but can be selectively limited by passing False
120        to specific arguments.
121        """
122        player = self.getplayer(bs.Player)
123        assert player
124
125        # Reset any currently connected player and/or the player we're
126        # wiring up.
127        if self._connected_to_player:
128            if player != self._connected_to_player:
129                player.resetinput()
130            self.disconnect_controls_from_player()
131        else:
132            player.resetinput()
133
134        player.assigninput(bs.InputType.UP_DOWN, self.on_move_up_down)
135        player.assigninput(bs.InputType.LEFT_RIGHT, self.on_move_left_right)
136        player.assigninput(
137            bs.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press
138        )
139        player.assigninput(
140            bs.InputType.HOLD_POSITION_RELEASE,
141            self.on_hold_position_release,
142        )
143        intp = bs.InputType
144        if enable_jump:
145            player.assigninput(intp.JUMP_PRESS, self.on_jump_press)
146            player.assigninput(intp.JUMP_RELEASE, self.on_jump_release)
147        if enable_pickup:
148            player.assigninput(intp.PICK_UP_PRESS, self.on_pickup_press)
149            player.assigninput(intp.PICK_UP_RELEASE, self.on_pickup_release)
150        if enable_punch:
151            player.assigninput(intp.PUNCH_PRESS, self.on_punch_press)
152            player.assigninput(intp.PUNCH_RELEASE, self.on_punch_release)
153        if enable_bomb:
154            player.assigninput(intp.BOMB_PRESS, self.on_bomb_press)
155            player.assigninput(intp.BOMB_RELEASE, self.on_bomb_release)
156        if enable_run:
157            player.assigninput(intp.RUN, self.on_run)
158        if enable_fly:
159            player.assigninput(intp.FLY_PRESS, self.on_fly_press)
160            player.assigninput(intp.FLY_RELEASE, self.on_fly_release)
161
162        self._connected_to_player = player

Wire this spaz up to the provided bascenev1.Player.

Full control of the character is given by default but can be selectively limited by passing False to specific arguments.

def disconnect_controls_from_player(self) -> None:
164    def disconnect_controls_from_player(self) -> None:
165        """
166        Completely sever any previously connected
167        bascenev1.Player from control of this spaz.
168        """
169        if self._connected_to_player:
170            self._connected_to_player.resetinput()
171            self._connected_to_player = None
172
173            # Send releases for anything in case its held.
174            self.on_move_up_down(0)
175            self.on_move_left_right(0)
176            self.on_hold_position_release()
177            self.on_jump_release()
178            self.on_pickup_release()
179            self.on_punch_release()
180            self.on_bomb_release()
181            self.on_run(0.0)
182            self.on_fly_release()
183        else:
184            print(
185                'WARNING: disconnect_controls_from_player() called for'
186                ' non-connected player'
187            )

Completely sever any previously connected bascenev1.Player from control of this spaz.

@override
def handlemessage(self, msg: Any) -> Any:
189    @override
190    def handlemessage(self, msg: Any) -> Any:
191        # FIXME: Tidy this up.
192        # pylint: disable=too-many-branches
193        # pylint: disable=too-many-statements
194        # pylint: disable=too-many-nested-blocks
195        assert not self.expired
196
197        # Keep track of if we're being held and by who most recently.
198        if isinstance(msg, bs.PickedUpMessage):
199            # Augment standard behavior.
200            super().handlemessage(msg)
201            self.held_count += 1
202            picked_up_by = msg.node.source_player
203            if picked_up_by:
204                self.last_player_held_by = picked_up_by
205        elif isinstance(msg, bs.DroppedMessage):
206            # Augment standard behavior.
207            super().handlemessage(msg)
208            self.held_count -= 1
209            if self.held_count < 0:
210                print('ERROR: spaz held_count < 0')
211
212            # Let's count someone dropping us as an attack.
213            picked_up_by = msg.node.source_player
214            if picked_up_by:
215                self.last_player_attacked_by = picked_up_by
216                self.last_attacked_time = bs.time()
217                self.last_attacked_type = ('picked_up', 'default')
218        elif isinstance(msg, bs.StandMessage):
219            super().handlemessage(msg)  # Augment standard behavior.
220
221            # Our Spaz was just moved somewhere. Explicitly update
222            # our associated player's position in case it is being used
223            # for logic (otherwise it will be out of date until next step)
224            self._drive_player_position()
225
226        elif isinstance(msg, bs.DieMessage):
227            # Report player deaths to the game.
228            if not self._dead:
229                # Immediate-mode or left-game deaths don't count as 'kills'.
230                killed = (
231                    not msg.immediate and msg.how is not bs.DeathType.LEFT_GAME
232                )
233
234                activity = self._activity()
235
236                player = self.getplayer(bs.Player, False)
237                if not killed:
238                    killerplayer = None
239                else:
240                    # If this player was being held at the time of death,
241                    # the holder is the killer.
242                    if self.held_count > 0 and self.last_player_held_by:
243                        killerplayer = self.last_player_held_by
244                    else:
245                        # Otherwise, if they were attacked by someone in the
246                        # last few seconds, that person is the killer.
247                        # Otherwise it was a suicide.
248                        # FIXME: Currently disabling suicides in Co-Op since
249                        #  all bot kills would register as suicides; need to
250                        #  change this from last_player_attacked_by to
251                        #  something like last_actor_attacked_by to fix that.
252                        if (
253                            self.last_player_attacked_by
254                            and bs.time() - self.last_attacked_time < 4.0
255                        ):
256                            killerplayer = self.last_player_attacked_by
257                        else:
258                            # ok, call it a suicide unless we're in co-op
259                            if activity is not None and not isinstance(
260                                activity.session, bs.CoopSession
261                            ):
262                                killerplayer = player
263                            else:
264                                killerplayer = None
265
266                # We should never wind up with a dead-reference here;
267                # we want to use None in that case.
268                assert killerplayer is None or killerplayer
269
270                # Only report if both the player and the activity still exist.
271                if killed and activity is not None and player:
272                    activity.handlemessage(
273                        bs.PlayerDiedMessage(
274                            player, killed, killerplayer, msg.how
275                        )
276                    )
277
278            super().handlemessage(msg)  # Augment standard behavior.
279
280        # Keep track of the player who last hit us for point rewarding.
281        elif isinstance(msg, bs.HitMessage):
282            source_player = msg.get_source_player(type(self._player))
283            if source_player:
284                self.last_player_attacked_by = source_player
285                self.last_attacked_time = bs.time()
286                self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
287            super().handlemessage(msg)  # Augment standard behavior.
288            activity = self._activity()
289            if activity is not None and self._player.exists():
290                activity.handlemessage(PlayerSpazHurtMessage(self))
291        else:
292            return super().handlemessage(msg)
293        return None

General message handling; can be passed any message object.