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

Game where players try to hit targets with bombs.

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

Instantiate the Activity.

default_music = <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) or issubclass(
60            sessiontype, ba.MultiTeamSession
61        )

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

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

Called when a new ba.Team joins the Activity.

(including the initial set of Teams)

def on_begin(self) -> None:
77    def on_begin(self) -> None:
78        super().on_begin()
79        self.update_scoreboard()
80
81        # Number of targets is based on player count.
82        for i in range(self._target_count):
83            ba.timer(5.0 + i * 1.0, self._spawn_target)
84
85        self._update_timer = ba.Timer(1.0, self._update, repeat=True)
86        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
87        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:
 89    def spawn_player(self, player: Player) -> ba.Actor:
 90        spawn_center = (0, 3, -5)
 91        pos = (
 92            spawn_center[0] + random.uniform(-1.5, 1.5),
 93            spawn_center[1],
 94            spawn_center[2] + random.uniform(-1.5, 1.5),
 95        )
 96
 97        # Reset their streak.
 98        player.streak = 0
 99        spaz = self.spawn_player_spaz(player, position=pos)
100
101        # Give players permanent triple impact bombs and wire them up
102        # to tell us when they drop a bomb.
103        if self._enable_impact_bombs:
104            spaz.bomb_type = 'impact'
105        if self._enable_triple_bombs:
106            spaz.set_bomb_count(3)
107        spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb)
108        return spaz

Spawn something for the provided ba.Player.

The default implementation simply calls spawn_player_spaz().

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

General message handling; can be passed any message object.

def update_scoreboard(self) -> None:
187    def update_scoreboard(self) -> None:
188        """Update the game scoreboard with current team values."""
189        for team in self.teams:
190            self._scoreboard.set_team_value(team, team.score)

Update the game scoreboard with current team values.

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

A target practice target.

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

Instantiates an Actor in the current ba.Activity.

def exists(self) -> bool:
256    def exists(self) -> bool:
257        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:
259    def handlemessage(self, msg: Any) -> Any:
260        if isinstance(msg, ba.DieMessage):
261            for node in self._nodes:
262                node.delete()
263            self._nodes = []
264        else:
265            super().handlemessage(msg)

General message handling; can be passed any message object.

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

Inform an object a target was hit.

Target.TargetHitMessage()