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

Our player type for this game.

streak
class Team(bascenev1._team.Team[bascenev1lib.game.targetpractice.Player]):
34class Team(bs.Team[Player]):
35    """Our team type for this game."""
36
37    def __init__(self) -> None:
38        self.score = 0

Our team type for this game.

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

Game where players try to hit targets with bombs.

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

Instantiate the Activity.

name = 'Target Practice'
description = 'Bomb as many targets as you can.'
available_settings = [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 = <MusicType.FORWARD_MARCH: 'ForwardMarch'>
@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1.Session]) -> list[str]:
54    @override
55    @classmethod
56    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
57        return ['Doom Shroom']

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

@override
@classmethod
def supports_session_type(cls, sessiontype: type[bascenev1.Session]) -> bool:
59    @override
60    @classmethod
61    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
62        # We support any teams or versus sessions.
63        return issubclass(sessiontype, bs.CoopSession) or issubclass(
64            sessiontype, bs.MultiTeamSession
65        )

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

@override
def on_team_join(self, team: Team) -> None:
77    @override
78    def on_team_join(self, team: Team) -> None:
79        if self.has_begun():
80            self.update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def on_begin(self) -> None:
82    @override
83    def on_begin(self) -> None:
84        super().on_begin()
85        self.update_scoreboard()
86
87        # Number of targets is based on player count.
88        for i in range(self._target_count):
89            bs.timer(5.0 + i * 1.0, self._spawn_target)
90
91        self._update_timer = bs.Timer(1.0, self._update, repeat=True)
92        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
93        bs.timer(4.0, self._countdown.start)

Called once the previous 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.

@override
def spawn_player( self, player: Player) -> bascenev1.Actor:
 95    @override
 96    def spawn_player(self, player: Player) -> bs.Actor:
 97        spawn_center = (0, 3, -5)
 98        pos = (
 99            spawn_center[0] + random.uniform(-1.5, 1.5),
100            spawn_center[1],
101            spawn_center[2] + random.uniform(-1.5, 1.5),
102        )
103
104        # Reset their streak.
105        player.streak = 0
106        spaz = self.spawn_player_spaz(player, position=pos)
107
108        # Give players permanent triple impact bombs and wire them up
109        # to tell us when they drop a bomb.
110        if self._enable_impact_bombs:
111            spaz.bomb_type = 'impact'
112        if self._enable_triple_bombs:
113            spaz.set_bomb_count(3)
114        spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb)
115        return spaz

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

@override
def handlemessage(self, msg: Any) -> Any:
179    @override
180    def handlemessage(self, msg: Any) -> Any:
181        # When players die, respawn them.
182        if isinstance(msg, bs.PlayerDiedMessage):
183            super().handlemessage(msg)  # Do standard stuff.
184            player = msg.getplayer(Player)
185            assert player is not None
186            self.respawn_player(player)  # Kick off a respawn.
187        elif isinstance(msg, Target.TargetHitMessage):
188            # A target is telling us it was hit and will die soon..
189            # ..so make another one.
190            self._spawn_target()
191        else:
192            super().handlemessage(msg)

General message handling; can be passed any message object.

def update_scoreboard(self) -> None:
194    def update_scoreboard(self) -> None:
195        """Update the game scoreboard with current team values."""
196        for team in self.teams:
197            self._scoreboard.set_team_value(team, team.score)

Update the game scoreboard with current team values.

@override
def end_game(self) -> None:
199    @override
200    def end_game(self) -> None:
201        results = bs.GameResults()
202        for team in self.teams:
203            results.set_team_score(team, team.score)
204        self.end(results)

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

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 (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.

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

A target practice target.

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

Instantiates an Actor in the current bascenev1.Activity.

@override
def exists(self) -> bool:
264    @override
265    def exists(self) -> bool:
266        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 bascenev1.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 bascenev1.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.

@override
def handlemessage(self, msg: Any) -> Any:
268    @override
269    def handlemessage(self, msg: Any) -> Any:
270        if isinstance(msg, bs.DieMessage):
271            for node in self._nodes:
272                node.delete()
273            self._nodes = []
274        else:
275            super().handlemessage(msg)

General message handling; can be passed any message object.

def get_dist_from_point(self, pos: _babase.Vec3) -> float:
277    def get_dist_from_point(self, pos: bs.Vec3) -> float:
278        """Given a point, returns distance squared from it."""
279        return (pos - self._position).length()

Given a point, returns distance squared from it.

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

Handle a bomb hit at the given position.

class Target.TargetHitMessage:
210    class TargetHitMessage:
211        """Inform an object a target was hit."""

Inform an object a target was hit.