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

A message saying a PlayerSpaz was hurt.

Category: Message Classes

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

Instantiate with the given bascenev1.Spaz value.

spaz: PlayerSpaz

The PlayerSpaz that was hurt

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

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

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

General message handling; can be passed any message object.