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        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    @overload
 84    def getplayer(
 85        self, playertype: type[PlayerT], doraise: Literal[True]
 86    ) -> PlayerT: ...
 87
 88    def getplayer(
 89        self, playertype: type[PlayerT], doraise: bool = False
 90    ) -> PlayerT | None:
 91        """Get the bascenev1.Player associated with this Spaz.
 92
 93        By default this will return None if the Player no longer exists.
 94        If you are logically certain that the Player still exists, pass
 95        doraise=False to get a non-optional return type.
 96        """
 97        player: Any = self._player
 98        assert isinstance(player, playertype)
 99        if not player.exists() and doraise:
100            raise bs.PlayerNotFoundError()
101        return player if player.exists() else None
102
103    def connect_controls_to_player(
104        self,
105        enable_jump: bool = True,
106        enable_punch: bool = True,
107        enable_pickup: bool = True,
108        enable_bomb: bool = True,
109        enable_run: bool = True,
110        enable_fly: bool = True,
111    ) -> None:
112        """Wire this spaz up to the provided bascenev1.Player.
113
114        Full control of the character is given by default
115        but can be selectively limited by passing False
116        to specific arguments.
117        """
118        player = self.getplayer(bs.Player)
119        assert player
120
121        # Reset any currently connected player and/or the player we're
122        # wiring up.
123        if self._connected_to_player:
124            if player != self._connected_to_player:
125                player.resetinput()
126            self.disconnect_controls_from_player()
127        else:
128            player.resetinput()
129
130        player.assigninput(bs.InputType.UP_DOWN, self.on_move_up_down)
131        player.assigninput(bs.InputType.LEFT_RIGHT, self.on_move_left_right)
132        player.assigninput(
133            bs.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press
134        )
135        player.assigninput(
136            bs.InputType.HOLD_POSITION_RELEASE,
137            self.on_hold_position_release,
138        )
139        intp = bs.InputType
140        if enable_jump:
141            player.assigninput(intp.JUMP_PRESS, self.on_jump_press)
142            player.assigninput(intp.JUMP_RELEASE, self.on_jump_release)
143        if enable_pickup:
144            player.assigninput(intp.PICK_UP_PRESS, self.on_pickup_press)
145            player.assigninput(intp.PICK_UP_RELEASE, self.on_pickup_release)
146        if enable_punch:
147            player.assigninput(intp.PUNCH_PRESS, self.on_punch_press)
148            player.assigninput(intp.PUNCH_RELEASE, self.on_punch_release)
149        if enable_bomb:
150            player.assigninput(intp.BOMB_PRESS, self.on_bomb_press)
151            player.assigninput(intp.BOMB_RELEASE, self.on_bomb_release)
152        if enable_run:
153            player.assigninput(intp.RUN, self.on_run)
154        if enable_fly:
155            player.assigninput(intp.FLY_PRESS, self.on_fly_press)
156            player.assigninput(intp.FLY_RELEASE, self.on_fly_release)
157
158        self._connected_to_player = player
159
160    def disconnect_controls_from_player(self) -> None:
161        """
162        Completely sever any previously connected
163        bascenev1.Player from control of this spaz.
164        """
165        if self._connected_to_player:
166            self._connected_to_player.resetinput()
167            self._connected_to_player = None
168
169            # Send releases for anything in case its held.
170            self.on_move_up_down(0)
171            self.on_move_left_right(0)
172            self.on_hold_position_release()
173            self.on_jump_release()
174            self.on_pickup_release()
175            self.on_punch_release()
176            self.on_bomb_release()
177            self.on_run(0.0)
178            self.on_fly_release()
179        else:
180            print(
181                'WARNING: disconnect_controls_from_player() called for'
182                ' non-connected player'
183            )
184
185    @override
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                # Was this player killed while being held?
226                was_held = self.held_count > 0 and self.last_player_held_by
227                # Was this player attacked before death?
228                was_attacked_recently = (
229                    self.last_player_attacked_by
230                    and bs.time() - self.last_attacked_time < 4.0
231                )
232                # Leaving the game doesn't count as a kill *unless*
233                # someone does it intentionally while being attacked.
234                left_game_cleanly = msg.how is bs.DeathType.LEFT_GAME and not (
235                    was_held or was_attacked_recently
236                )
237
238                killed = not (msg.immediate or left_game_cleanly)
239
240                activity = self._activity()
241
242                player = self.getplayer(bs.Player, False)
243                if not killed:
244                    killerplayer = None
245                else:
246                    # If this player was being held at the time of death,
247                    # the holder is the killer.
248                    if was_held:
249                        killerplayer = self.last_player_held_by
250                    else:
251                        # Otherwise, if they were attacked by someone in the
252                        # last few seconds, that person is the killer.
253                        # Otherwise it was a suicide.
254                        # FIXME: Currently disabling suicides in Co-Op since
255                        #  all bot kills would register as suicides; need to
256                        #  change this from last_player_attacked_by to
257                        #  something like last_actor_attacked_by to fix that.
258                        if was_attacked_recently:
259                            killerplayer = self.last_player_attacked_by
260                        else:
261                            # ok, call it a suicide unless we're in co-op
262                            if activity is not None and not isinstance(
263                                activity.session, bs.CoopSession
264                            ):
265                                killerplayer = player
266                            else:
267                                killerplayer = None
268
269                # We should never wind up with a dead-reference here;
270                # we want to use None in that case.
271                assert killerplayer is None or killerplayer
272
273                # Only report if both the player and the activity still exist.
274                if killed and activity is not None and player:
275                    activity.handlemessage(
276                        bs.PlayerDiedMessage(
277                            player, killed, killerplayer, msg.how
278                        )
279                    )
280
281            super().handlemessage(msg)  # Augment standard behavior.
282
283        # Keep track of the player who last hit us for point rewarding.
284        elif isinstance(msg, bs.HitMessage):
285            source_player = msg.get_source_player(type(self._player))
286            if source_player:
287                self.last_player_attacked_by = source_player
288                self.last_attacked_time = bs.time()
289                self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
290            super().handlemessage(msg)  # Augment standard behavior.
291            activity = self._activity()
292            if activity is not None and self._player.exists():
293                activity.handlemessage(PlayerSpazHurtMessage(self))
294        else:
295            return super().handlemessage(msg)
296        return None
297
298    def _drive_player_position(self) -> None:
299        """Drive our bascenev1.Player's official position
300
301        If our position is changed explicitly, this should be called again
302        to instantly update the player position (otherwise it would be out
303        of date until the next sim step)
304        """
305        player = self._player
306        if player:
307            assert self.node
308            assert player.node
309            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        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        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    @override
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                # Was this player killed while being held?
227                was_held = self.held_count > 0 and self.last_player_held_by
228                # Was this player attacked before death?
229                was_attacked_recently = (
230                    self.last_player_attacked_by
231                    and bs.time() - self.last_attacked_time < 4.0
232                )
233                # Leaving the game doesn't count as a kill *unless*
234                # someone does it intentionally while being attacked.
235                left_game_cleanly = msg.how is bs.DeathType.LEFT_GAME and not (
236                    was_held or was_attacked_recently
237                )
238
239                killed = not (msg.immediate or left_game_cleanly)
240
241                activity = self._activity()
242
243                player = self.getplayer(bs.Player, False)
244                if not killed:
245                    killerplayer = None
246                else:
247                    # If this player was being held at the time of death,
248                    # the holder is the killer.
249                    if was_held:
250                        killerplayer = self.last_player_held_by
251                    else:
252                        # Otherwise, if they were attacked by someone in the
253                        # last few seconds, that person is the killer.
254                        # Otherwise it was a suicide.
255                        # FIXME: Currently disabling suicides in Co-Op since
256                        #  all bot kills would register as suicides; need to
257                        #  change this from last_player_attacked_by to
258                        #  something like last_actor_attacked_by to fix that.
259                        if was_attacked_recently:
260                            killerplayer = self.last_player_attacked_by
261                        else:
262                            # ok, call it a suicide unless we're in co-op
263                            if activity is not None and not isinstance(
264                                activity.session, bs.CoopSession
265                            ):
266                                killerplayer = player
267                            else:
268                                killerplayer = None
269
270                # We should never wind up with a dead-reference here;
271                # we want to use None in that case.
272                assert killerplayer is None or killerplayer
273
274                # Only report if both the player and the activity still exist.
275                if killed and activity is not None and player:
276                    activity.handlemessage(
277                        bs.PlayerDiedMessage(
278                            player, killed, killerplayer, msg.how
279                        )
280                    )
281
282            super().handlemessage(msg)  # Augment standard behavior.
283
284        # Keep track of the player who last hit us for point rewarding.
285        elif isinstance(msg, bs.HitMessage):
286            source_player = msg.get_source_player(type(self._player))
287            if source_player:
288                self.last_player_attacked_by = source_player
289                self.last_attacked_time = bs.time()
290                self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
291            super().handlemessage(msg)  # Augment standard behavior.
292            activity = self._activity()
293            if activity is not None and self._player.exists():
294                activity.handlemessage(PlayerSpazHurtMessage(self))
295        else:
296            return super().handlemessage(msg)
297        return None
298
299    def _drive_player_position(self) -> None:
300        """Drive our bascenev1.Player's official position
301
302        If our position is changed explicitly, this should be called again
303        to instantly update the player position (otherwise it would be out
304        of date until the next sim step)
305        """
306        player = self._player
307        if player:
308            assert self.node
309            assert player.node
310            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        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()

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

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

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

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

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

General message handling; can be passed any message object.