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

A message saying a PlayerSpaz was hurt.

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

Instantiate with the given bascenev1.Spaz value.

spaz: PlayerSpaz

The PlayerSpaz that was hurt

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

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

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, *, 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)
42    def __init__(
43        self,
44        player: bs.Player,
45        *,
46        color: Sequence[float] = (1.0, 1.0, 1.0),
47        highlight: Sequence[float] = (0.5, 0.5, 0.5),
48        character: str = 'Spaz',
49        powerups_expire: bool = True,
50    ):
51        """Create a spaz for the provided bascenev1.Player.
52
53        Note: this does not wire up any controls;
54        you must call connect_controls_to_player() to do so.
55        """
56
57        super().__init__(
58            color=color,
59            highlight=highlight,
60            character=character,
61            source_player=player,
62            start_invincible=True,
63            powerups_expire=powerups_expire,
64        )
65        self.last_player_attacked_by: bs.Player | None = None
66        self.last_attacked_time = 0.0
67        self.last_attacked_type: tuple[str, str] | None = None
68        self.held_count = 0
69        self.last_player_held_by: bs.Player | None = None
70        self._player = player
71        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 | None
last_attacked_time
last_attacked_type: tuple[str, str] | None
held_count
last_player_held_by: bascenev1.Player | None
def getplayer( self, playertype: type[~PlayerT], doraise: bool = False) -> Optional[~PlayerT]:
85    def getplayer(
86        self, playertype: type[PlayerT], doraise: bool = False
87    ) -> PlayerT | None:
88        """Get the bascenev1.Player associated with this Spaz.
89
90        By default this will return None if the Player no longer exists.
91        If you are logically certain that the Player still exists, pass
92        doraise=False to get a non-optional return type.
93        """
94        player: Any = self._player
95        assert isinstance(player, playertype)
96        if not player.exists() and doraise:
97            raise bs.PlayerNotFoundError()
98        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:
100    def connect_controls_to_player(
101        self,
102        *,
103        enable_jump: bool = True,
104        enable_punch: bool = True,
105        enable_pickup: bool = True,
106        enable_bomb: bool = True,
107        enable_run: bool = True,
108        enable_fly: bool = True,
109    ) -> None:
110        """Wire this spaz up to the provided bascenev1.Player.
111
112        Full control of the character is given by default
113        but can be selectively limited by passing False
114        to specific arguments.
115        """
116        player = self.getplayer(bs.Player)
117        assert player
118
119        # Reset any currently connected player and/or the player we're
120        # wiring up.
121        if self._connected_to_player:
122            if player != self._connected_to_player:
123                player.resetinput()
124            self.disconnect_controls_from_player()
125        else:
126            player.resetinput()
127
128        player.assigninput(bs.InputType.UP_DOWN, self.on_move_up_down)
129        player.assigninput(bs.InputType.LEFT_RIGHT, self.on_move_left_right)
130        player.assigninput(
131            bs.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press
132        )
133        player.assigninput(
134            bs.InputType.HOLD_POSITION_RELEASE,
135            self.on_hold_position_release,
136        )
137        intp = bs.InputType
138        if enable_jump:
139            player.assigninput(intp.JUMP_PRESS, self.on_jump_press)
140            player.assigninput(intp.JUMP_RELEASE, self.on_jump_release)
141        if enable_pickup:
142            player.assigninput(intp.PICK_UP_PRESS, self.on_pickup_press)
143            player.assigninput(intp.PICK_UP_RELEASE, self.on_pickup_release)
144        if enable_punch:
145            player.assigninput(intp.PUNCH_PRESS, self.on_punch_press)
146            player.assigninput(intp.PUNCH_RELEASE, self.on_punch_release)
147        if enable_bomb:
148            player.assigninput(intp.BOMB_PRESS, self.on_bomb_press)
149            player.assigninput(intp.BOMB_RELEASE, self.on_bomb_release)
150        if enable_run:
151            player.assigninput(intp.RUN, self.on_run)
152        if enable_fly:
153            player.assigninput(intp.FLY_PRESS, self.on_fly_press)
154            player.assigninput(intp.FLY_RELEASE, self.on_fly_release)
155
156        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:
158    def disconnect_controls_from_player(self) -> None:
159        """
160        Completely sever any previously connected
161        bascenev1.Player from control of this spaz.
162        """
163        if self._connected_to_player:
164            self._connected_to_player.resetinput()
165            self._connected_to_player = None
166
167            # Send releases for anything in case its held.
168            self.on_move_up_down(0)
169            self.on_move_left_right(0)
170            self.on_hold_position_release()
171            self.on_jump_release()
172            self.on_pickup_release()
173            self.on_punch_release()
174            self.on_bomb_release()
175            self.on_run(0.0)
176            self.on_fly_release()
177        else:
178            print(
179                'WARNING: disconnect_controls_from_player() called for'
180                ' non-connected player'
181            )

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

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

General message handling; can be passed any message object.