bastd.game.ninjafight

Provides Ninja Fight mini-game.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides Ninja Fight mini-game."""
  4
  5# ba_meta require api 7
  6# (see https://ballistica.net/wiki/meta-tag-system)
  7
  8from __future__ import annotations
  9
 10import random
 11from typing import TYPE_CHECKING
 12
 13import ba
 14from bastd.actor.spazbot import SpazBotSet, ChargerBot, SpazBotDiedMessage
 15from bastd.actor.onscreentimer import OnScreenTimer
 16
 17if TYPE_CHECKING:
 18    from typing import Any
 19
 20
 21class Player(ba.Player['Team']):
 22    """Our player type for this game."""
 23
 24
 25class Team(ba.Team[Player]):
 26    """Our team type for this game."""
 27
 28
 29# ba_meta export game
 30class NinjaFightGame(ba.TeamGameActivity[Player, Team]):
 31    """
 32    A co-op game where you try to defeat a group
 33    of Ninjas as fast as possible
 34    """
 35
 36    name = 'Ninja Fight'
 37    description = 'How fast can you defeat the ninjas?'
 38    scoreconfig = ba.ScoreConfig(
 39        label='Time', scoretype=ba.ScoreType.MILLISECONDS, lower_is_better=True
 40    )
 41    default_music = ba.MusicType.TO_THE_DEATH
 42
 43    @classmethod
 44    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
 45        # For now we're hard-coding spawn positions and whatnot
 46        # so we need to be sure to specify that we only support
 47        # a specific map.
 48        return ['Courtyard']
 49
 50    @classmethod
 51    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
 52        # We currently support Co-Op only.
 53        return issubclass(sessiontype, ba.CoopSession)
 54
 55    # In the constructor we should load any media we need/etc.
 56    # ...but not actually create anything yet.
 57    def __init__(self, settings: dict):
 58        super().__init__(settings)
 59        self._winsound = ba.getsound('score')
 60        self._won = False
 61        self._timer: OnScreenTimer | None = None
 62        self._bots = SpazBotSet()
 63        self._preset = str(settings['preset'])
 64
 65    # Called when our game actually begins.
 66    def on_begin(self) -> None:
 67        super().on_begin()
 68        is_pro = self._preset == 'pro'
 69
 70        # In pro mode there's no powerups.
 71        if not is_pro:
 72            self.setup_standard_powerup_drops()
 73
 74        # Make our on-screen timer and start it roughly when our bots appear.
 75        self._timer = OnScreenTimer()
 76        ba.timer(4.0, self._timer.start)
 77
 78        # Spawn some baddies.
 79        ba.timer(
 80            1.0,
 81            lambda: self._bots.spawn_bot(
 82                ChargerBot, pos=(3, 3, -2), spawn_time=3.0
 83            ),
 84        )
 85        ba.timer(
 86            2.0,
 87            lambda: self._bots.spawn_bot(
 88                ChargerBot, pos=(-3, 3, -2), spawn_time=3.0
 89            ),
 90        )
 91        ba.timer(
 92            3.0,
 93            lambda: self._bots.spawn_bot(
 94                ChargerBot, pos=(5, 3, -2), spawn_time=3.0
 95            ),
 96        )
 97        ba.timer(
 98            4.0,
 99            lambda: self._bots.spawn_bot(
100                ChargerBot, pos=(-5, 3, -2), spawn_time=3.0
101            ),
102        )
103
104        # Add some extras for multiplayer or pro mode.
105        assert self.initialplayerinfos is not None
106        if len(self.initialplayerinfos) > 2 or is_pro:
107            ba.timer(
108                5.0,
109                lambda: self._bots.spawn_bot(
110                    ChargerBot, pos=(0, 3, -5), spawn_time=3.0
111                ),
112            )
113        if len(self.initialplayerinfos) > 3 or is_pro:
114            ba.timer(
115                6.0,
116                lambda: self._bots.spawn_bot(
117                    ChargerBot, pos=(0, 3, 1), spawn_time=3.0
118                ),
119            )
120
121    # Called for each spawning player.
122    def spawn_player(self, player: Player) -> ba.Actor:
123
124        # Let's spawn close to the center.
125        spawn_center = (0, 3, -2)
126        pos = (
127            spawn_center[0] + random.uniform(-1.5, 1.5),
128            spawn_center[1],
129            spawn_center[2] + random.uniform(-1.5, 1.5),
130        )
131        return self.spawn_player_spaz(player, position=pos)
132
133    def _check_if_won(self) -> None:
134        # Simply end the game if there's no living bots.
135        # FIXME: Should also make sure all bots have been spawned;
136        #  if spawning is spread out enough that we're able to kill
137        #  all living bots before the next spawns, it would incorrectly
138        #  count as a win.
139        if not self._bots.have_living_bots():
140            self._won = True
141            self.end_game()
142
143    # Called for miscellaneous messages.
144    def handlemessage(self, msg: Any) -> Any:
145
146        # A player has died.
147        if isinstance(msg, ba.PlayerDiedMessage):
148            super().handlemessage(msg)  # Augment standard behavior.
149            self.respawn_player(msg.getplayer(Player))
150
151        # A spaz-bot has died.
152        elif isinstance(msg, SpazBotDiedMessage):
153            # Unfortunately the bot-set will always tell us there are living
154            # bots if we ask here (the currently-dying bot isn't officially
155            # marked dead yet) ..so lets push a call into the event loop to
156            # check once this guy has finished dying.
157            ba.pushcall(self._check_if_won)
158
159        # Let the base class handle anything we don't.
160        else:
161            return super().handlemessage(msg)
162        return None
163
164    # When this is called, we should fill out results and end the game
165    # *regardless* of whether is has been won. (this may be called due
166    # to a tournament ending or other external reason).
167    def end_game(self) -> None:
168
169        # Stop our on-screen timer so players can see what they got.
170        assert self._timer is not None
171        self._timer.stop()
172
173        results = ba.GameResults()
174
175        # If we won, set our score to the elapsed time in milliseconds.
176        # (there should just be 1 team here since this is co-op).
177        # ..if we didn't win, leave scores as default (None) which means
178        # we lost.
179        if self._won:
180            elapsed_time_ms = int((ba.time() - self._timer.starttime) * 1000.0)
181            ba.cameraflash()
182            ba.playsound(self._winsound)
183            for team in self.teams:
184                for player in team.players:
185                    if player.actor:
186                        player.actor.handlemessage(ba.CelebrateMessage())
187                results.set_team_score(team, elapsed_time_ms)
188
189        # Ends the activity.
190        self.end(results)
class Player(ba._player.Player[ForwardRef('Team')]):
22class Player(ba.Player['Team']):
23    """Our player type for this game."""

Our player type for this game.

Player()
Inherited Members
ba._player.Player
actor
on_expire
team
customdata
sessionplayer
node
position
exists
getname
is_alive
get_icon
assigninput
resetinput
class Team(ba._team.Team[bastd.game.ninjafight.Player]):
26class Team(ba.Team[Player]):
27    """Our team type for this game."""

Our team type for this game.

Team()
Inherited Members
ba._team.Team
manual_init
customdata
on_expire
sessionteam
class NinjaFightGame(ba._teamgame.TeamGameActivity[bastd.game.ninjafight.Player, bastd.game.ninjafight.Team]):
 31class NinjaFightGame(ba.TeamGameActivity[Player, Team]):
 32    """
 33    A co-op game where you try to defeat a group
 34    of Ninjas as fast as possible
 35    """
 36
 37    name = 'Ninja Fight'
 38    description = 'How fast can you defeat the ninjas?'
 39    scoreconfig = ba.ScoreConfig(
 40        label='Time', scoretype=ba.ScoreType.MILLISECONDS, lower_is_better=True
 41    )
 42    default_music = ba.MusicType.TO_THE_DEATH
 43
 44    @classmethod
 45    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
 46        # For now we're hard-coding spawn positions and whatnot
 47        # so we need to be sure to specify that we only support
 48        # a specific map.
 49        return ['Courtyard']
 50
 51    @classmethod
 52    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
 53        # We currently support Co-Op only.
 54        return issubclass(sessiontype, ba.CoopSession)
 55
 56    # In the constructor we should load any media we need/etc.
 57    # ...but not actually create anything yet.
 58    def __init__(self, settings: dict):
 59        super().__init__(settings)
 60        self._winsound = ba.getsound('score')
 61        self._won = False
 62        self._timer: OnScreenTimer | None = None
 63        self._bots = SpazBotSet()
 64        self._preset = str(settings['preset'])
 65
 66    # Called when our game actually begins.
 67    def on_begin(self) -> None:
 68        super().on_begin()
 69        is_pro = self._preset == 'pro'
 70
 71        # In pro mode there's no powerups.
 72        if not is_pro:
 73            self.setup_standard_powerup_drops()
 74
 75        # Make our on-screen timer and start it roughly when our bots appear.
 76        self._timer = OnScreenTimer()
 77        ba.timer(4.0, self._timer.start)
 78
 79        # Spawn some baddies.
 80        ba.timer(
 81            1.0,
 82            lambda: self._bots.spawn_bot(
 83                ChargerBot, pos=(3, 3, -2), spawn_time=3.0
 84            ),
 85        )
 86        ba.timer(
 87            2.0,
 88            lambda: self._bots.spawn_bot(
 89                ChargerBot, pos=(-3, 3, -2), spawn_time=3.0
 90            ),
 91        )
 92        ba.timer(
 93            3.0,
 94            lambda: self._bots.spawn_bot(
 95                ChargerBot, pos=(5, 3, -2), spawn_time=3.0
 96            ),
 97        )
 98        ba.timer(
 99            4.0,
100            lambda: self._bots.spawn_bot(
101                ChargerBot, pos=(-5, 3, -2), spawn_time=3.0
102            ),
103        )
104
105        # Add some extras for multiplayer or pro mode.
106        assert self.initialplayerinfos is not None
107        if len(self.initialplayerinfos) > 2 or is_pro:
108            ba.timer(
109                5.0,
110                lambda: self._bots.spawn_bot(
111                    ChargerBot, pos=(0, 3, -5), spawn_time=3.0
112                ),
113            )
114        if len(self.initialplayerinfos) > 3 or is_pro:
115            ba.timer(
116                6.0,
117                lambda: self._bots.spawn_bot(
118                    ChargerBot, pos=(0, 3, 1), spawn_time=3.0
119                ),
120            )
121
122    # Called for each spawning player.
123    def spawn_player(self, player: Player) -> ba.Actor:
124
125        # Let's spawn close to the center.
126        spawn_center = (0, 3, -2)
127        pos = (
128            spawn_center[0] + random.uniform(-1.5, 1.5),
129            spawn_center[1],
130            spawn_center[2] + random.uniform(-1.5, 1.5),
131        )
132        return self.spawn_player_spaz(player, position=pos)
133
134    def _check_if_won(self) -> None:
135        # Simply end the game if there's no living bots.
136        # FIXME: Should also make sure all bots have been spawned;
137        #  if spawning is spread out enough that we're able to kill
138        #  all living bots before the next spawns, it would incorrectly
139        #  count as a win.
140        if not self._bots.have_living_bots():
141            self._won = True
142            self.end_game()
143
144    # Called for miscellaneous messages.
145    def handlemessage(self, msg: Any) -> Any:
146
147        # A player has died.
148        if isinstance(msg, ba.PlayerDiedMessage):
149            super().handlemessage(msg)  # Augment standard behavior.
150            self.respawn_player(msg.getplayer(Player))
151
152        # A spaz-bot has died.
153        elif isinstance(msg, SpazBotDiedMessage):
154            # Unfortunately the bot-set will always tell us there are living
155            # bots if we ask here (the currently-dying bot isn't officially
156            # marked dead yet) ..so lets push a call into the event loop to
157            # check once this guy has finished dying.
158            ba.pushcall(self._check_if_won)
159
160        # Let the base class handle anything we don't.
161        else:
162            return super().handlemessage(msg)
163        return None
164
165    # When this is called, we should fill out results and end the game
166    # *regardless* of whether is has been won. (this may be called due
167    # to a tournament ending or other external reason).
168    def end_game(self) -> None:
169
170        # Stop our on-screen timer so players can see what they got.
171        assert self._timer is not None
172        self._timer.stop()
173
174        results = ba.GameResults()
175
176        # If we won, set our score to the elapsed time in milliseconds.
177        # (there should just be 1 team here since this is co-op).
178        # ..if we didn't win, leave scores as default (None) which means
179        # we lost.
180        if self._won:
181            elapsed_time_ms = int((ba.time() - self._timer.starttime) * 1000.0)
182            ba.cameraflash()
183            ba.playsound(self._winsound)
184            for team in self.teams:
185                for player in team.players:
186                    if player.actor:
187                        player.actor.handlemessage(ba.CelebrateMessage())
188                results.set_team_score(team, elapsed_time_ms)
189
190        # Ends the activity.
191        self.end(results)

A co-op game where you try to defeat a group of Ninjas as fast as possible

NinjaFightGame(settings: dict)
58    def __init__(self, settings: dict):
59        super().__init__(settings)
60        self._winsound = ba.getsound('score')
61        self._won = False
62        self._timer: OnScreenTimer | None = None
63        self._bots = SpazBotSet()
64        self._preset = str(settings['preset'])

Instantiate the Activity.

default_music = <MusicType.TO_THE_DEATH: 'ToTheDeath'>
@classmethod
def get_supported_maps(cls, sessiontype: type[ba._session.Session]) -> list[str]:
44    @classmethod
45    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
46        # For now we're hard-coding spawn positions and whatnot
47        # so we need to be sure to specify that we only support
48        # a specific map.
49        return ['Courtyard']

Called by the default ba.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given ba.Session type.

@classmethod
def supports_session_type(cls, sessiontype: type[ba._session.Session]) -> bool:
51    @classmethod
52    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
53        # We currently support Co-Op only.
54        return issubclass(sessiontype, ba.CoopSession)

Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.

def on_begin(self) -> None:
 67    def on_begin(self) -> None:
 68        super().on_begin()
 69        is_pro = self._preset == 'pro'
 70
 71        # In pro mode there's no powerups.
 72        if not is_pro:
 73            self.setup_standard_powerup_drops()
 74
 75        # Make our on-screen timer and start it roughly when our bots appear.
 76        self._timer = OnScreenTimer()
 77        ba.timer(4.0, self._timer.start)
 78
 79        # Spawn some baddies.
 80        ba.timer(
 81            1.0,
 82            lambda: self._bots.spawn_bot(
 83                ChargerBot, pos=(3, 3, -2), spawn_time=3.0
 84            ),
 85        )
 86        ba.timer(
 87            2.0,
 88            lambda: self._bots.spawn_bot(
 89                ChargerBot, pos=(-3, 3, -2), spawn_time=3.0
 90            ),
 91        )
 92        ba.timer(
 93            3.0,
 94            lambda: self._bots.spawn_bot(
 95                ChargerBot, pos=(5, 3, -2), spawn_time=3.0
 96            ),
 97        )
 98        ba.timer(
 99            4.0,
100            lambda: self._bots.spawn_bot(
101                ChargerBot, pos=(-5, 3, -2), spawn_time=3.0
102            ),
103        )
104
105        # Add some extras for multiplayer or pro mode.
106        assert self.initialplayerinfos is not None
107        if len(self.initialplayerinfos) > 2 or is_pro:
108            ba.timer(
109                5.0,
110                lambda: self._bots.spawn_bot(
111                    ChargerBot, pos=(0, 3, -5), spawn_time=3.0
112                ),
113            )
114        if len(self.initialplayerinfos) > 3 or is_pro:
115            ba.timer(
116                6.0,
117                lambda: self._bots.spawn_bot(
118                    ChargerBot, pos=(0, 3, 1), spawn_time=3.0
119                ),
120            )

Called once the previous ba.Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

def spawn_player(self, player: bastd.game.ninjafight.Player) -> ba._actor.Actor:
123    def spawn_player(self, player: Player) -> ba.Actor:
124
125        # Let's spawn close to the center.
126        spawn_center = (0, 3, -2)
127        pos = (
128            spawn_center[0] + random.uniform(-1.5, 1.5),
129            spawn_center[1],
130            spawn_center[2] + random.uniform(-1.5, 1.5),
131        )
132        return self.spawn_player_spaz(player, position=pos)

Spawn something for the provided ba.Player.

The default implementation simply calls spawn_player_spaz().

def handlemessage(self, msg: Any) -> Any:
145    def handlemessage(self, msg: Any) -> Any:
146
147        # A player has died.
148        if isinstance(msg, ba.PlayerDiedMessage):
149            super().handlemessage(msg)  # Augment standard behavior.
150            self.respawn_player(msg.getplayer(Player))
151
152        # A spaz-bot has died.
153        elif isinstance(msg, SpazBotDiedMessage):
154            # Unfortunately the bot-set will always tell us there are living
155            # bots if we ask here (the currently-dying bot isn't officially
156            # marked dead yet) ..so lets push a call into the event loop to
157            # check once this guy has finished dying.
158            ba.pushcall(self._check_if_won)
159
160        # Let the base class handle anything we don't.
161        else:
162            return super().handlemessage(msg)
163        return None

General message handling; can be passed any message object.

def end_game(self) -> None:
168    def end_game(self) -> None:
169
170        # Stop our on-screen timer so players can see what they got.
171        assert self._timer is not None
172        self._timer.stop()
173
174        results = ba.GameResults()
175
176        # If we won, set our score to the elapsed time in milliseconds.
177        # (there should just be 1 team here since this is co-op).
178        # ..if we didn't win, leave scores as default (None) which means
179        # we lost.
180        if self._won:
181            elapsed_time_ms = int((ba.time() - self._timer.starttime) * 1000.0)
182            ba.cameraflash()
183            ba.playsound(self._winsound)
184            for team in self.teams:
185                for player in team.players:
186                    if player.actor:
187                        player.actor.handlemessage(ba.CelebrateMessage())
188                results.set_team_score(team, elapsed_time_ms)
189
190        # Ends the activity.
191        self.end(results)

Tell the game to wrap up and call ba.Activity.end() immediately.

This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (ba.GameActivity.setup_standard_time_limit()) will work with the game.

Inherited Members
ba._teamgame.TeamGameActivity
on_transition_in
spawn_player_spaz
end
ba._gameactivity.GameActivity
allow_pausing
allow_kick_idle_players
create_settings_ui
getscoreconfig
getname
get_display_string
get_team_display_string
get_description
get_description_display_string
get_available_settings
get_settings_display_string
map
get_instance_display_string
get_instance_scoreboard_display_string
get_instance_description
get_instance_description_short
on_continue
is_waiting_for_continue
continue_or_end_game
on_player_join
respawn_player
spawn_player_if_exists
setup_standard_powerup_drops
setup_standard_time_limit
show_zoom_message
ba._activity.Activity
settings_raw
teams
players
announce_player_deaths
is_joining_activity
use_fixed_vr_overlay
slow_motion
inherits_slow_motion
inherits_music
inherits_vr_camera_offset
inherits_vr_overlay_center
inherits_tint
allow_mid_activity_joins
transition_time
can_show_ad_on_death
globalsnode
stats
on_expire
customdata
expired
playertype
teamtype
retain_actor
add_actor_weak_ref
session
on_player_leave
on_team_join
on_team_leave
on_transition_out
has_transitioned_in
has_begun
has_ended
is_transitioning_out
transition_out
create_player
create_team
ba._dependency.DependencyComponent
dep_is_present
get_dynamic_deps