bastd.game.targetpractice

Implements Target Practice game.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Implements Target Practice 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.scoreboard import Scoreboard
 15from bastd.actor.onscreencountdown import OnScreenCountdown
 16from bastd.actor.bomb import Bomb
 17from bastd.actor.popuptext import PopupText
 18
 19if TYPE_CHECKING:
 20    from typing import Any, Sequence
 21    from bastd.actor.bomb import Blast
 22
 23
 24class Player(ba.Player['Team']):
 25    """Our player type for this game."""
 26
 27    def __init__(self) -> None:
 28        self.streak = 0
 29
 30
 31class Team(ba.Team[Player]):
 32    """Our team type for this game."""
 33
 34    def __init__(self) -> None:
 35        self.score = 0
 36
 37
 38# ba_meta export game
 39class TargetPracticeGame(ba.TeamGameActivity[Player, Team]):
 40    """Game where players try to hit targets with bombs."""
 41
 42    name = 'Target Practice'
 43    description = 'Bomb as many targets as you can.'
 44    available_settings = [
 45        ba.IntSetting('Target Count', min_value=1, default=3),
 46        ba.BoolSetting('Enable Impact Bombs', default=True),
 47        ba.BoolSetting('Enable Triple Bombs', default=True)
 48    ]
 49    default_music = ba.MusicType.FORWARD_MARCH
 50
 51    @classmethod
 52    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
 53        return ['Doom Shroom']
 54
 55    @classmethod
 56    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
 57        # We support any teams or versus sessions.
 58        return (issubclass(sessiontype, ba.CoopSession)
 59                or issubclass(sessiontype, ba.MultiTeamSession))
 60
 61    def __init__(self, settings: dict):
 62        super().__init__(settings)
 63        self._scoreboard = Scoreboard()
 64        self._targets: list[Target] = []
 65        self._update_timer: ba.Timer | None = None
 66        self._countdown: OnScreenCountdown | None = None
 67        self._target_count = int(settings['Target Count'])
 68        self._enable_impact_bombs = bool(settings['Enable Impact Bombs'])
 69        self._enable_triple_bombs = bool(settings['Enable Triple Bombs'])
 70
 71    def on_team_join(self, team: Team) -> None:
 72        if self.has_begun():
 73            self.update_scoreboard()
 74
 75    def on_begin(self) -> None:
 76        super().on_begin()
 77        self.update_scoreboard()
 78
 79        # Number of targets is based on player count.
 80        for i in range(self._target_count):
 81            ba.timer(5.0 + i * 1.0, self._spawn_target)
 82
 83        self._update_timer = ba.Timer(1.0, self._update, repeat=True)
 84        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
 85        ba.timer(4.0, self._countdown.start)
 86
 87    def spawn_player(self, player: Player) -> ba.Actor:
 88        spawn_center = (0, 3, -5)
 89        pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1],
 90               spawn_center[2] + random.uniform(-1.5, 1.5))
 91
 92        # Reset their streak.
 93        player.streak = 0
 94        spaz = self.spawn_player_spaz(player, position=pos)
 95
 96        # Give players permanent triple impact bombs and wire them up
 97        # to tell us when they drop a bomb.
 98        if self._enable_impact_bombs:
 99            spaz.bomb_type = 'impact'
100        if self._enable_triple_bombs:
101            spaz.set_bomb_count(3)
102        spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb)
103        return spaz
104
105    def _spawn_target(self) -> None:
106
107        # Generate a few random points; we'll use whichever one is farthest
108        # from our existing targets (don't want overlapping targets).
109        points = []
110
111        for _i in range(4):
112            # Calc a random point within a circle.
113            while True:
114                xpos = random.uniform(-1.0, 1.0)
115                ypos = random.uniform(-1.0, 1.0)
116                if xpos * xpos + ypos * ypos < 1.0:
117                    break
118            points.append(ba.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos))
119
120        def get_min_dist_from_target(pnt: ba.Vec3) -> float:
121            return min((t.get_dist_from_point(pnt) for t in self._targets))
122
123        # If we have existing targets, use the point with the highest
124        # min-distance-from-targets.
125        if self._targets:
126            point = max(points, key=get_min_dist_from_target)
127        else:
128            point = points[0]
129
130        self._targets.append(Target(position=point))
131
132    def _on_spaz_dropped_bomb(self, spaz: ba.Actor, bomb: ba.Actor) -> None:
133        del spaz  # Unused.
134
135        # Wire up this bomb to inform us when it blows up.
136        assert isinstance(bomb, Bomb)
137        bomb.add_explode_callback(self._on_bomb_exploded)
138
139    def _on_bomb_exploded(self, bomb: Bomb, blast: Blast) -> None:
140        assert blast.node
141        pos = blast.node.position
142
143        # Debugging: throw a locator down where we landed.
144        # ba.newnode('locator', attrs={'position':blast.node.position})
145
146        # Feed the explosion point to all our targets and get points in return.
147        # Note: we operate on a copy of self._targets since the list may change
148        # under us if we hit stuff (don't wanna get points for new targets).
149        player = bomb.get_source_player(Player)
150        if not player:
151            # It's possible the player left after throwing the bomb.
152            return
153
154        bullseye = any(
155            target.do_hit_at_position(pos, player)
156            for target in list(self._targets))
157        if bullseye:
158            player.streak += 1
159        else:
160            player.streak = 0
161
162    def _update(self) -> None:
163        """Misc. periodic updating."""
164        # Clear out targets that have died.
165        self._targets = [t for t in self._targets if t]
166
167    def handlemessage(self, msg: Any) -> Any:
168        # When players die, respawn them.
169        if isinstance(msg, ba.PlayerDiedMessage):
170            super().handlemessage(msg)  # Do standard stuff.
171            player = msg.getplayer(Player)
172            assert player is not None
173            self.respawn_player(player)  # Kick off a respawn.
174        elif isinstance(msg, Target.TargetHitMessage):
175            # A target is telling us it was hit and will die soon..
176            # ..so make another one.
177            self._spawn_target()
178        else:
179            super().handlemessage(msg)
180
181    def update_scoreboard(self) -> None:
182        """Update the game scoreboard with current team values."""
183        for team in self.teams:
184            self._scoreboard.set_team_value(team, team.score)
185
186    def end_game(self) -> None:
187        results = ba.GameResults()
188        for team in self.teams:
189            results.set_team_score(team, team.score)
190        self.end(results)
191
192
193class Target(ba.Actor):
194    """A target practice target."""
195
196    class TargetHitMessage:
197        """Inform an object a target was hit."""
198
199    def __init__(self, position: Sequence[float]):
200        self._r1 = 0.45
201        self._r2 = 1.1
202        self._r3 = 2.0
203        self._rfudge = 0.15
204        super().__init__()
205        self._position = ba.Vec3(position)
206        self._hit = False
207
208        # It can be handy to test with this on to make sure the projection
209        # isn't too far off from the actual object.
210        show_in_space = False
211        loc1 = ba.newnode('locator',
212                          attrs={
213                              'shape': 'circle',
214                              'position': position,
215                              'color': (0, 1, 0),
216                              'opacity': 0.5,
217                              'draw_beauty': show_in_space,
218                              'additive': True
219                          })
220        loc2 = ba.newnode('locator',
221                          attrs={
222                              'shape': 'circleOutline',
223                              'position': position,
224                              'color': (0, 1, 0),
225                              'opacity': 0.3,
226                              'draw_beauty': False,
227                              'additive': True
228                          })
229        loc3 = ba.newnode('locator',
230                          attrs={
231                              'shape': 'circleOutline',
232                              'position': position,
233                              'color': (0, 1, 0),
234                              'opacity': 0.1,
235                              'draw_beauty': False,
236                              'additive': True
237                          })
238        self._nodes = [loc1, loc2, loc3]
239        ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]})
240        ba.animate_array(loc2, 'size', 1, {
241            0.05: [0.0],
242            0.25: [self._r2 * 2.0]
243        })
244        ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]})
245        ba.playsound(ba.getsound('laserReverse'))
246
247    def exists(self) -> bool:
248        return bool(self._nodes)
249
250    def handlemessage(self, msg: Any) -> Any:
251        if isinstance(msg, ba.DieMessage):
252            for node in self._nodes:
253                node.delete()
254            self._nodes = []
255        else:
256            super().handlemessage(msg)
257
258    def get_dist_from_point(self, pos: ba.Vec3) -> float:
259        """Given a point, returns distance squared from it."""
260        return (pos - self._position).length()
261
262    def do_hit_at_position(self, pos: Sequence[float], player: Player) -> bool:
263        """Handle a bomb hit at the given position."""
264        # pylint: disable=too-many-statements
265        activity = self.activity
266
267        # Ignore hits if the game is over or if we've already been hit
268        if activity.has_ended() or self._hit or not self._nodes:
269            return False
270
271        diff = (ba.Vec3(pos) - self._position)
272
273        # Disregard Y difference. Our target point probably isn't exactly
274        # on the ground anyway.
275        diff[1] = 0.0
276        dist = diff.length()
277
278        bullseye = False
279        if dist <= self._r3 + self._rfudge:
280            # Inform our activity that we were hit
281            self._hit = True
282            activity.handlemessage(self.TargetHitMessage())
283            keys: dict[float, Sequence[float]] = {
284                0.0: (1.0, 0.0, 0.0),
285                0.049: (1.0, 0.0, 0.0),
286                0.05: (1.0, 1.0, 1.0),
287                0.1: (0.0, 1.0, 0.0)
288            }
289            cdull = (0.3, 0.3, 0.3)
290            popupcolor: Sequence[float]
291            if dist <= self._r1 + self._rfudge:
292                bullseye = True
293                self._nodes[1].color = cdull
294                self._nodes[2].color = cdull
295                ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True)
296                popupscale = 1.8
297                popupcolor = (1, 1, 0, 1)
298                streak = player.streak
299                points = 10 + min(20, streak * 2)
300                ba.playsound(ba.getsound('bellHigh'))
301                if streak > 0:
302                    ba.playsound(
303                        ba.getsound(
304                            'orchestraHit4' if streak > 3 else
305                            'orchestraHit3' if streak > 2 else
306                            'orchestraHit2' if streak > 1 else 'orchestraHit'))
307            elif dist <= self._r2 + self._rfudge:
308                self._nodes[0].color = cdull
309                self._nodes[2].color = cdull
310                ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True)
311                popupscale = 1.25
312                popupcolor = (1, 0.5, 0.2, 1)
313                points = 4
314                ba.playsound(ba.getsound('bellMed'))
315            else:
316                self._nodes[0].color = cdull
317                self._nodes[1].color = cdull
318                ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True)
319                popupscale = 1.0
320                popupcolor = (0.8, 0.3, 0.3, 1)
321                points = 2
322                ba.playsound(ba.getsound('bellLow'))
323
324            # Award points/etc.. (technically should probably leave this up
325            # to the activity).
326            popupstr = '+' + str(points)
327
328            # If there's more than 1 player in the game, include their
329            # names and colors so they know who got the hit.
330            if len(activity.players) > 1:
331                popupcolor = ba.safecolor(player.color, target_intensity=0.75)
332                popupstr += ' ' + player.getname()
333            PopupText(popupstr,
334                      position=self._position,
335                      color=popupcolor,
336                      scale=popupscale).autoretain()
337
338            # Give this player's team points and update the score-board.
339            player.team.score += points
340            assert isinstance(activity, TargetPracticeGame)
341            activity.update_scoreboard()
342
343            # Also give this individual player points
344            # (only applies in teams mode).
345            assert activity.stats is not None
346            activity.stats.player_scored(player,
347                                         points,
348                                         showpoints=False,
349                                         screenmessage=False)
350
351            ba.animate_array(self._nodes[0], 'size', 1, {
352                0.8: self._nodes[0].size,
353                1.0: [0.0]
354            })
355            ba.animate_array(self._nodes[1], 'size', 1, {
356                0.85: self._nodes[1].size,
357                1.05: [0.0]
358            })
359            ba.animate_array(self._nodes[2], 'size', 1, {
360                0.9: self._nodes[2].size,
361                1.1: [0.0]
362            })
363            ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage()))
364
365        return bullseye
class Player(ba._player.Player[ForwardRef('Team')]):
25class Player(ba.Player['Team']):
26    """Our player type for this game."""
27
28    def __init__(self) -> None:
29        self.streak = 0

Our player type for this game.

Player()
28    def __init__(self) -> None:
29        self.streak = 0
class Team(ba._team.Team[bastd.game.targetpractice.Player]):
32class Team(ba.Team[Player]):
33    """Our team type for this game."""
34
35    def __init__(self) -> None:
36        self.score = 0

Our team type for this game.

Team()
35    def __init__(self) -> None:
36        self.score = 0
Inherited Members
ba._team.Team
manual_init
customdata
on_expire
sessionteam
class TargetPracticeGame(ba._teamgame.TeamGameActivity[bastd.game.targetpractice.Player, bastd.game.targetpractice.Team]):
 40class TargetPracticeGame(ba.TeamGameActivity[Player, Team]):
 41    """Game where players try to hit targets with bombs."""
 42
 43    name = 'Target Practice'
 44    description = 'Bomb as many targets as you can.'
 45    available_settings = [
 46        ba.IntSetting('Target Count', min_value=1, default=3),
 47        ba.BoolSetting('Enable Impact Bombs', default=True),
 48        ba.BoolSetting('Enable Triple Bombs', default=True)
 49    ]
 50    default_music = ba.MusicType.FORWARD_MARCH
 51
 52    @classmethod
 53    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
 54        return ['Doom Shroom']
 55
 56    @classmethod
 57    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
 58        # We support any teams or versus sessions.
 59        return (issubclass(sessiontype, ba.CoopSession)
 60                or issubclass(sessiontype, ba.MultiTeamSession))
 61
 62    def __init__(self, settings: dict):
 63        super().__init__(settings)
 64        self._scoreboard = Scoreboard()
 65        self._targets: list[Target] = []
 66        self._update_timer: ba.Timer | None = None
 67        self._countdown: OnScreenCountdown | None = None
 68        self._target_count = int(settings['Target Count'])
 69        self._enable_impact_bombs = bool(settings['Enable Impact Bombs'])
 70        self._enable_triple_bombs = bool(settings['Enable Triple Bombs'])
 71
 72    def on_team_join(self, team: Team) -> None:
 73        if self.has_begun():
 74            self.update_scoreboard()
 75
 76    def on_begin(self) -> None:
 77        super().on_begin()
 78        self.update_scoreboard()
 79
 80        # Number of targets is based on player count.
 81        for i in range(self._target_count):
 82            ba.timer(5.0 + i * 1.0, self._spawn_target)
 83
 84        self._update_timer = ba.Timer(1.0, self._update, repeat=True)
 85        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
 86        ba.timer(4.0, self._countdown.start)
 87
 88    def spawn_player(self, player: Player) -> ba.Actor:
 89        spawn_center = (0, 3, -5)
 90        pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1],
 91               spawn_center[2] + random.uniform(-1.5, 1.5))
 92
 93        # Reset their streak.
 94        player.streak = 0
 95        spaz = self.spawn_player_spaz(player, position=pos)
 96
 97        # Give players permanent triple impact bombs and wire them up
 98        # to tell us when they drop a bomb.
 99        if self._enable_impact_bombs:
100            spaz.bomb_type = 'impact'
101        if self._enable_triple_bombs:
102            spaz.set_bomb_count(3)
103        spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb)
104        return spaz
105
106    def _spawn_target(self) -> None:
107
108        # Generate a few random points; we'll use whichever one is farthest
109        # from our existing targets (don't want overlapping targets).
110        points = []
111
112        for _i in range(4):
113            # Calc a random point within a circle.
114            while True:
115                xpos = random.uniform(-1.0, 1.0)
116                ypos = random.uniform(-1.0, 1.0)
117                if xpos * xpos + ypos * ypos < 1.0:
118                    break
119            points.append(ba.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos))
120
121        def get_min_dist_from_target(pnt: ba.Vec3) -> float:
122            return min((t.get_dist_from_point(pnt) for t in self._targets))
123
124        # If we have existing targets, use the point with the highest
125        # min-distance-from-targets.
126        if self._targets:
127            point = max(points, key=get_min_dist_from_target)
128        else:
129            point = points[0]
130
131        self._targets.append(Target(position=point))
132
133    def _on_spaz_dropped_bomb(self, spaz: ba.Actor, bomb: ba.Actor) -> None:
134        del spaz  # Unused.
135
136        # Wire up this bomb to inform us when it blows up.
137        assert isinstance(bomb, Bomb)
138        bomb.add_explode_callback(self._on_bomb_exploded)
139
140    def _on_bomb_exploded(self, bomb: Bomb, blast: Blast) -> None:
141        assert blast.node
142        pos = blast.node.position
143
144        # Debugging: throw a locator down where we landed.
145        # ba.newnode('locator', attrs={'position':blast.node.position})
146
147        # Feed the explosion point to all our targets and get points in return.
148        # Note: we operate on a copy of self._targets since the list may change
149        # under us if we hit stuff (don't wanna get points for new targets).
150        player = bomb.get_source_player(Player)
151        if not player:
152            # It's possible the player left after throwing the bomb.
153            return
154
155        bullseye = any(
156            target.do_hit_at_position(pos, player)
157            for target in list(self._targets))
158        if bullseye:
159            player.streak += 1
160        else:
161            player.streak = 0
162
163    def _update(self) -> None:
164        """Misc. periodic updating."""
165        # Clear out targets that have died.
166        self._targets = [t for t in self._targets if t]
167
168    def handlemessage(self, msg: Any) -> Any:
169        # When players die, respawn them.
170        if isinstance(msg, ba.PlayerDiedMessage):
171            super().handlemessage(msg)  # Do standard stuff.
172            player = msg.getplayer(Player)
173            assert player is not None
174            self.respawn_player(player)  # Kick off a respawn.
175        elif isinstance(msg, Target.TargetHitMessage):
176            # A target is telling us it was hit and will die soon..
177            # ..so make another one.
178            self._spawn_target()
179        else:
180            super().handlemessage(msg)
181
182    def update_scoreboard(self) -> None:
183        """Update the game scoreboard with current team values."""
184        for team in self.teams:
185            self._scoreboard.set_team_value(team, team.score)
186
187    def end_game(self) -> None:
188        results = ba.GameResults()
189        for team in self.teams:
190            results.set_team_score(team, team.score)
191        self.end(results)

Game where players try to hit targets with bombs.

TargetPracticeGame(settings: dict)
62    def __init__(self, settings: dict):
63        super().__init__(settings)
64        self._scoreboard = Scoreboard()
65        self._targets: list[Target] = []
66        self._update_timer: ba.Timer | None = None
67        self._countdown: OnScreenCountdown | None = None
68        self._target_count = int(settings['Target Count'])
69        self._enable_impact_bombs = bool(settings['Enable Impact Bombs'])
70        self._enable_triple_bombs = bool(settings['Enable Triple Bombs'])

Instantiate the Activity.

name: str | None = 'Target Practice'
description: str | None = 'Bomb as many targets as you can.'
available_settings: list[ba._settings.Setting] | None = [IntSetting(name='Target Count', default=3, min_value=1, max_value=9999, increment=1), BoolSetting(name='Enable Impact Bombs', default=True), BoolSetting(name='Enable Triple Bombs', default=True)]
default_music: ba._music.MusicType | None = <MusicType.FORWARD_MARCH: 'ForwardMarch'>
@classmethod
def get_supported_maps(cls, sessiontype: type[ba._session.Session]) -> list[str]:
52    @classmethod
53    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
54        return ['Doom Shroom']

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:
56    @classmethod
57    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
58        # We support any teams or versus sessions.
59        return (issubclass(sessiontype, ba.CoopSession)
60                or issubclass(sessiontype, ba.MultiTeamSession))

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

def on_team_join(self, team: bastd.game.targetpractice.Team) -> None:
72    def on_team_join(self, team: Team) -> None:
73        if self.has_begun():
74            self.update_scoreboard()

Called when a new ba.Team joins the Activity.

(including the initial set of Teams)

def on_begin(self) -> None:
76    def on_begin(self) -> None:
77        super().on_begin()
78        self.update_scoreboard()
79
80        # Number of targets is based on player count.
81        for i in range(self._target_count):
82            ba.timer(5.0 + i * 1.0, self._spawn_target)
83
84        self._update_timer = ba.Timer(1.0, self._update, repeat=True)
85        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
86        ba.timer(4.0, self._countdown.start)

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.targetpractice.Player) -> ba._actor.Actor:
 88    def spawn_player(self, player: Player) -> ba.Actor:
 89        spawn_center = (0, 3, -5)
 90        pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1],
 91               spawn_center[2] + random.uniform(-1.5, 1.5))
 92
 93        # Reset their streak.
 94        player.streak = 0
 95        spaz = self.spawn_player_spaz(player, position=pos)
 96
 97        # Give players permanent triple impact bombs and wire them up
 98        # to tell us when they drop a bomb.
 99        if self._enable_impact_bombs:
100            spaz.bomb_type = 'impact'
101        if self._enable_triple_bombs:
102            spaz.set_bomb_count(3)
103        spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb)
104        return spaz

Spawn something for the provided ba.Player.

The default implementation simply calls spawn_player_spaz().

def handlemessage(self, msg: Any) -> Any:
168    def handlemessage(self, msg: Any) -> Any:
169        # When players die, respawn them.
170        if isinstance(msg, ba.PlayerDiedMessage):
171            super().handlemessage(msg)  # Do standard stuff.
172            player = msg.getplayer(Player)
173            assert player is not None
174            self.respawn_player(player)  # Kick off a respawn.
175        elif isinstance(msg, Target.TargetHitMessage):
176            # A target is telling us it was hit and will die soon..
177            # ..so make another one.
178            self._spawn_target()
179        else:
180            super().handlemessage(msg)

General message handling; can be passed any message object.

def update_scoreboard(self) -> None:
182    def update_scoreboard(self) -> None:
183        """Update the game scoreboard with current team values."""
184        for team in self.teams:
185            self._scoreboard.set_team_value(team, team.score)

Update the game scoreboard with current team values.

def end_game(self) -> None:
187    def end_game(self) -> None:
188        results = ba.GameResults()
189        for team in self.teams:
190            results.set_team_score(team, team.score)
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
tips
scoreconfig
allow_pausing
allow_kick_idle_players
show_kill_points
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_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
class Target(ba._actor.Actor):
194class Target(ba.Actor):
195    """A target practice target."""
196
197    class TargetHitMessage:
198        """Inform an object a target was hit."""
199
200    def __init__(self, position: Sequence[float]):
201        self._r1 = 0.45
202        self._r2 = 1.1
203        self._r3 = 2.0
204        self._rfudge = 0.15
205        super().__init__()
206        self._position = ba.Vec3(position)
207        self._hit = False
208
209        # It can be handy to test with this on to make sure the projection
210        # isn't too far off from the actual object.
211        show_in_space = False
212        loc1 = ba.newnode('locator',
213                          attrs={
214                              'shape': 'circle',
215                              'position': position,
216                              'color': (0, 1, 0),
217                              'opacity': 0.5,
218                              'draw_beauty': show_in_space,
219                              'additive': True
220                          })
221        loc2 = ba.newnode('locator',
222                          attrs={
223                              'shape': 'circleOutline',
224                              'position': position,
225                              'color': (0, 1, 0),
226                              'opacity': 0.3,
227                              'draw_beauty': False,
228                              'additive': True
229                          })
230        loc3 = ba.newnode('locator',
231                          attrs={
232                              'shape': 'circleOutline',
233                              'position': position,
234                              'color': (0, 1, 0),
235                              'opacity': 0.1,
236                              'draw_beauty': False,
237                              'additive': True
238                          })
239        self._nodes = [loc1, loc2, loc3]
240        ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]})
241        ba.animate_array(loc2, 'size', 1, {
242            0.05: [0.0],
243            0.25: [self._r2 * 2.0]
244        })
245        ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]})
246        ba.playsound(ba.getsound('laserReverse'))
247
248    def exists(self) -> bool:
249        return bool(self._nodes)
250
251    def handlemessage(self, msg: Any) -> Any:
252        if isinstance(msg, ba.DieMessage):
253            for node in self._nodes:
254                node.delete()
255            self._nodes = []
256        else:
257            super().handlemessage(msg)
258
259    def get_dist_from_point(self, pos: ba.Vec3) -> float:
260        """Given a point, returns distance squared from it."""
261        return (pos - self._position).length()
262
263    def do_hit_at_position(self, pos: Sequence[float], player: Player) -> bool:
264        """Handle a bomb hit at the given position."""
265        # pylint: disable=too-many-statements
266        activity = self.activity
267
268        # Ignore hits if the game is over or if we've already been hit
269        if activity.has_ended() or self._hit or not self._nodes:
270            return False
271
272        diff = (ba.Vec3(pos) - self._position)
273
274        # Disregard Y difference. Our target point probably isn't exactly
275        # on the ground anyway.
276        diff[1] = 0.0
277        dist = diff.length()
278
279        bullseye = False
280        if dist <= self._r3 + self._rfudge:
281            # Inform our activity that we were hit
282            self._hit = True
283            activity.handlemessage(self.TargetHitMessage())
284            keys: dict[float, Sequence[float]] = {
285                0.0: (1.0, 0.0, 0.0),
286                0.049: (1.0, 0.0, 0.0),
287                0.05: (1.0, 1.0, 1.0),
288                0.1: (0.0, 1.0, 0.0)
289            }
290            cdull = (0.3, 0.3, 0.3)
291            popupcolor: Sequence[float]
292            if dist <= self._r1 + self._rfudge:
293                bullseye = True
294                self._nodes[1].color = cdull
295                self._nodes[2].color = cdull
296                ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True)
297                popupscale = 1.8
298                popupcolor = (1, 1, 0, 1)
299                streak = player.streak
300                points = 10 + min(20, streak * 2)
301                ba.playsound(ba.getsound('bellHigh'))
302                if streak > 0:
303                    ba.playsound(
304                        ba.getsound(
305                            'orchestraHit4' if streak > 3 else
306                            'orchestraHit3' if streak > 2 else
307                            'orchestraHit2' if streak > 1 else 'orchestraHit'))
308            elif dist <= self._r2 + self._rfudge:
309                self._nodes[0].color = cdull
310                self._nodes[2].color = cdull
311                ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True)
312                popupscale = 1.25
313                popupcolor = (1, 0.5, 0.2, 1)
314                points = 4
315                ba.playsound(ba.getsound('bellMed'))
316            else:
317                self._nodes[0].color = cdull
318                self._nodes[1].color = cdull
319                ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True)
320                popupscale = 1.0
321                popupcolor = (0.8, 0.3, 0.3, 1)
322                points = 2
323                ba.playsound(ba.getsound('bellLow'))
324
325            # Award points/etc.. (technically should probably leave this up
326            # to the activity).
327            popupstr = '+' + str(points)
328
329            # If there's more than 1 player in the game, include their
330            # names and colors so they know who got the hit.
331            if len(activity.players) > 1:
332                popupcolor = ba.safecolor(player.color, target_intensity=0.75)
333                popupstr += ' ' + player.getname()
334            PopupText(popupstr,
335                      position=self._position,
336                      color=popupcolor,
337                      scale=popupscale).autoretain()
338
339            # Give this player's team points and update the score-board.
340            player.team.score += points
341            assert isinstance(activity, TargetPracticeGame)
342            activity.update_scoreboard()
343
344            # Also give this individual player points
345            # (only applies in teams mode).
346            assert activity.stats is not None
347            activity.stats.player_scored(player,
348                                         points,
349                                         showpoints=False,
350                                         screenmessage=False)
351
352            ba.animate_array(self._nodes[0], 'size', 1, {
353                0.8: self._nodes[0].size,
354                1.0: [0.0]
355            })
356            ba.animate_array(self._nodes[1], 'size', 1, {
357                0.85: self._nodes[1].size,
358                1.05: [0.0]
359            })
360            ba.animate_array(self._nodes[2], 'size', 1, {
361                0.9: self._nodes[2].size,
362                1.1: [0.0]
363            })
364            ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage()))
365
366        return bullseye

A target practice target.

Target(position: Sequence[float])
200    def __init__(self, position: Sequence[float]):
201        self._r1 = 0.45
202        self._r2 = 1.1
203        self._r3 = 2.0
204        self._rfudge = 0.15
205        super().__init__()
206        self._position = ba.Vec3(position)
207        self._hit = False
208
209        # It can be handy to test with this on to make sure the projection
210        # isn't too far off from the actual object.
211        show_in_space = False
212        loc1 = ba.newnode('locator',
213                          attrs={
214                              'shape': 'circle',
215                              'position': position,
216                              'color': (0, 1, 0),
217                              'opacity': 0.5,
218                              'draw_beauty': show_in_space,
219                              'additive': True
220                          })
221        loc2 = ba.newnode('locator',
222                          attrs={
223                              'shape': 'circleOutline',
224                              'position': position,
225                              'color': (0, 1, 0),
226                              'opacity': 0.3,
227                              'draw_beauty': False,
228                              'additive': True
229                          })
230        loc3 = ba.newnode('locator',
231                          attrs={
232                              'shape': 'circleOutline',
233                              'position': position,
234                              'color': (0, 1, 0),
235                              'opacity': 0.1,
236                              'draw_beauty': False,
237                              'additive': True
238                          })
239        self._nodes = [loc1, loc2, loc3]
240        ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]})
241        ba.animate_array(loc2, 'size', 1, {
242            0.05: [0.0],
243            0.25: [self._r2 * 2.0]
244        })
245        ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]})
246        ba.playsound(ba.getsound('laserReverse'))

Instantiates an Actor in the current ba.Activity.

def exists(self) -> bool:
248    def exists(self) -> bool:
249        return bool(self._nodes)

Returns whether the Actor is still present in a meaningful way.

Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' (see ba.Actor.is_alive() for that).

If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with ba.Actor.autoretain()

The default implementation of this method always return True.

Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.

def handlemessage(self, msg: Any) -> Any:
251    def handlemessage(self, msg: Any) -> Any:
252        if isinstance(msg, ba.DieMessage):
253            for node in self._nodes:
254                node.delete()
255            self._nodes = []
256        else:
257            super().handlemessage(msg)

General message handling; can be passed any message object.

def get_dist_from_point(self, pos: _ba.Vec3) -> float:
259    def get_dist_from_point(self, pos: ba.Vec3) -> float:
260        """Given a point, returns distance squared from it."""
261        return (pos - self._position).length()

Given a point, returns distance squared from it.

def do_hit_at_position( self, pos: Sequence[float], player: bastd.game.targetpractice.Player) -> bool:
263    def do_hit_at_position(self, pos: Sequence[float], player: Player) -> bool:
264        """Handle a bomb hit at the given position."""
265        # pylint: disable=too-many-statements
266        activity = self.activity
267
268        # Ignore hits if the game is over or if we've already been hit
269        if activity.has_ended() or self._hit or not self._nodes:
270            return False
271
272        diff = (ba.Vec3(pos) - self._position)
273
274        # Disregard Y difference. Our target point probably isn't exactly
275        # on the ground anyway.
276        diff[1] = 0.0
277        dist = diff.length()
278
279        bullseye = False
280        if dist <= self._r3 + self._rfudge:
281            # Inform our activity that we were hit
282            self._hit = True
283            activity.handlemessage(self.TargetHitMessage())
284            keys: dict[float, Sequence[float]] = {
285                0.0: (1.0, 0.0, 0.0),
286                0.049: (1.0, 0.0, 0.0),
287                0.05: (1.0, 1.0, 1.0),
288                0.1: (0.0, 1.0, 0.0)
289            }
290            cdull = (0.3, 0.3, 0.3)
291            popupcolor: Sequence[float]
292            if dist <= self._r1 + self._rfudge:
293                bullseye = True
294                self._nodes[1].color = cdull
295                self._nodes[2].color = cdull
296                ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True)
297                popupscale = 1.8
298                popupcolor = (1, 1, 0, 1)
299                streak = player.streak
300                points = 10 + min(20, streak * 2)
301                ba.playsound(ba.getsound('bellHigh'))
302                if streak > 0:
303                    ba.playsound(
304                        ba.getsound(
305                            'orchestraHit4' if streak > 3 else
306                            'orchestraHit3' if streak > 2 else
307                            'orchestraHit2' if streak > 1 else 'orchestraHit'))
308            elif dist <= self._r2 + self._rfudge:
309                self._nodes[0].color = cdull
310                self._nodes[2].color = cdull
311                ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True)
312                popupscale = 1.25
313                popupcolor = (1, 0.5, 0.2, 1)
314                points = 4
315                ba.playsound(ba.getsound('bellMed'))
316            else:
317                self._nodes[0].color = cdull
318                self._nodes[1].color = cdull
319                ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True)
320                popupscale = 1.0
321                popupcolor = (0.8, 0.3, 0.3, 1)
322                points = 2
323                ba.playsound(ba.getsound('bellLow'))
324
325            # Award points/etc.. (technically should probably leave this up
326            # to the activity).
327            popupstr = '+' + str(points)
328
329            # If there's more than 1 player in the game, include their
330            # names and colors so they know who got the hit.
331            if len(activity.players) > 1:
332                popupcolor = ba.safecolor(player.color, target_intensity=0.75)
333                popupstr += ' ' + player.getname()
334            PopupText(popupstr,
335                      position=self._position,
336                      color=popupcolor,
337                      scale=popupscale).autoretain()
338
339            # Give this player's team points and update the score-board.
340            player.team.score += points
341            assert isinstance(activity, TargetPracticeGame)
342            activity.update_scoreboard()
343
344            # Also give this individual player points
345            # (only applies in teams mode).
346            assert activity.stats is not None
347            activity.stats.player_scored(player,
348                                         points,
349                                         showpoints=False,
350                                         screenmessage=False)
351
352            ba.animate_array(self._nodes[0], 'size', 1, {
353                0.8: self._nodes[0].size,
354                1.0: [0.0]
355            })
356            ba.animate_array(self._nodes[1], 'size', 1, {
357                0.85: self._nodes[1].size,
358                1.05: [0.0]
359            })
360            ba.animate_array(self._nodes[2], 'size', 1, {
361                0.9: self._nodes[2].size,
362                1.1: [0.0]
363            })
364            ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage()))
365
366        return bullseye

Handle a bomb hit at the given position.

Inherited Members
ba._actor.Actor
autoretain
on_expire
expired
is_alive
activity
getactivity
class Target.TargetHitMessage:
197    class TargetHitMessage:
198        """Inform an object a target was hit."""

Inform an object a target was hit.

Target.TargetHitMessage()