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

A message saying a PlayerSpaz was hurt.

Category: Message Classes

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

Instantiate with the given bascenev1.Spaz value.

spaz: PlayerSpaz

The PlayerSpaz that was hurt

class PlayerSpaz(bascenev1lib.actor.spaz.Spaz):
 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        *,
 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    @overload
 86    def getplayer(
 87        self, playertype: type[PlayerT], doraise: Literal[True]
 88    ) -> PlayerT: ...
 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        *,
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                # Was this player killed while being held?
229                was_held = self.held_count > 0 and self.last_player_held_by
230                # Was this player attacked before death?
231                was_attacked_recently = (
232                    self.last_player_attacked_by
233                    and bs.time() - self.last_attacked_time < 4.0
234                )
235                # Leaving the game doesn't count as a kill *unless*
236                # someone does it intentionally while being attacked.
237                left_game_cleanly = msg.how is bs.DeathType.LEFT_GAME and not (
238                    was_held or was_attacked_recently
239                )
240
241                killed = not (msg.immediate or left_game_cleanly)
242
243                activity = self._activity()
244
245                player = self.getplayer(bs.Player, False)
246                if not killed:
247                    killerplayer = None
248                else:
249                    # If this player was being held at the time of death,
250                    # the holder is the killer.
251                    if was_held:
252                        killerplayer = self.last_player_held_by
253                    else:
254                        # Otherwise, if they were attacked by someone in the
255                        # last few seconds, that person is the killer.
256                        # Otherwise it was a suicide.
257                        # FIXME: Currently disabling suicides in Co-Op since
258                        #  all bot kills would register as suicides; need to
259                        #  change this from last_player_attacked_by to
260                        #  something like last_actor_attacked_by to fix that.
261                        if was_attacked_recently:
262                            killerplayer = self.last_player_attacked_by
263                        else:
264                            # ok, call it a suicide unless we're in co-op
265                            if activity is not None and not isinstance(
266                                activity.session, bs.CoopSession
267                            ):
268                                killerplayer = player
269                            else:
270                                killerplayer = None
271
272                # We should never wind up with a dead-reference here;
273                # we want to use None in that case.
274                assert killerplayer is None or killerplayer
275
276                # Only report if both the player and the activity still exist.
277                if killed and activity is not None and player:
278                    activity.handlemessage(
279                        bs.PlayerDiedMessage(
280                            player, killed, killerplayer, msg.how
281                        )
282                    )
283
284            super().handlemessage(msg)  # Augment standard behavior.
285
286        # Keep track of the player who last hit us for point rewarding.
287        elif isinstance(msg, bs.HitMessage):
288            source_player = msg.get_source_player(type(self._player))
289            if source_player:
290                self.last_player_attacked_by = source_player
291                self.last_attacked_time = bs.time()
292                self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
293            super().handlemessage(msg)  # Augment standard behavior.
294            activity = self._activity()
295            if activity is not None and self._player.exists():
296                activity.handlemessage(PlayerSpazHurtMessage(self))
297        else:
298            return super().handlemessage(msg)
299        return None
300
301    def _drive_player_position(self) -> None:
302        """Drive our bascenev1.Player's official position
303
304        If our position is changed explicitly, this should be called again
305        to instantly update the player position (otherwise it would be out
306        of date until the next sim step)
307        """
308        player = self._player
309        if player:
310            assert self.node
311            assert player.node
312            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, *, 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)
47    def __init__(
48        self,
49        player: bs.Player,
50        *,
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 | 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]:
 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        *,
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

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:
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            )

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

@override
def handlemessage(self, msg: Any) -> Any:
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                # Was this player killed while being held?
229                was_held = self.held_count > 0 and self.last_player_held_by
230                # Was this player attacked before death?
231                was_attacked_recently = (
232                    self.last_player_attacked_by
233                    and bs.time() - self.last_attacked_time < 4.0
234                )
235                # Leaving the game doesn't count as a kill *unless*
236                # someone does it intentionally while being attacked.
237                left_game_cleanly = msg.how is bs.DeathType.LEFT_GAME and not (
238                    was_held or was_attacked_recently
239                )
240
241                killed = not (msg.immediate or left_game_cleanly)
242
243                activity = self._activity()
244
245                player = self.getplayer(bs.Player, False)
246                if not killed:
247                    killerplayer = None
248                else:
249                    # If this player was being held at the time of death,
250                    # the holder is the killer.
251                    if was_held:
252                        killerplayer = self.last_player_held_by
253                    else:
254                        # Otherwise, if they were attacked by someone in the
255                        # last few seconds, that person is the killer.
256                        # Otherwise it was a suicide.
257                        # FIXME: Currently disabling suicides in Co-Op since
258                        #  all bot kills would register as suicides; need to
259                        #  change this from last_player_attacked_by to
260                        #  something like last_actor_attacked_by to fix that.
261                        if was_attacked_recently:
262                            killerplayer = self.last_player_attacked_by
263                        else:
264                            # ok, call it a suicide unless we're in co-op
265                            if activity is not None and not isinstance(
266                                activity.session, bs.CoopSession
267                            ):
268                                killerplayer = player
269                            else:
270                                killerplayer = None
271
272                # We should never wind up with a dead-reference here;
273                # we want to use None in that case.
274                assert killerplayer is None or killerplayer
275
276                # Only report if both the player and the activity still exist.
277                if killed and activity is not None and player:
278                    activity.handlemessage(
279                        bs.PlayerDiedMessage(
280                            player, killed, killerplayer, msg.how
281                        )
282                    )
283
284            super().handlemessage(msg)  # Augment standard behavior.
285
286        # Keep track of the player who last hit us for point rewarding.
287        elif isinstance(msg, bs.HitMessage):
288            source_player = msg.get_source_player(type(self._player))
289            if source_player:
290                self.last_player_attacked_by = source_player
291                self.last_attacked_time = bs.time()
292                self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
293            super().handlemessage(msg)  # Augment standard behavior.
294            activity = self._activity()
295            if activity is not None and self._player.exists():
296                activity.handlemessage(PlayerSpazHurtMessage(self))
297        else:
298            return super().handlemessage(msg)
299        return None

General message handling; can be passed any message object.