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 8
  6# (see https://ballistica.net/wiki/meta-tag-system)
  7
  8from __future__ import annotations
  9
 10import random
 11from typing import TYPE_CHECKING
 12
 13from typing_extensions import override
 14import bascenev1 as bs
 15
 16from bascenev1lib.actor.scoreboard import Scoreboard
 17from bascenev1lib.actor.onscreencountdown import OnScreenCountdown
 18from bascenev1lib.actor.bomb import Bomb
 19from bascenev1lib.actor.popuptext import PopupText
 20
 21if TYPE_CHECKING:
 22    from typing import Any, Sequence
 23
 24    from bascenev1lib.actor.bomb import Blast
 25
 26
 27class Player(bs.Player['Team']):
 28    """Our player type for this game."""
 29
 30    def __init__(self) -> None:
 31        self.streak = 0
 32
 33
 34class Team(bs.Team[Player]):
 35    """Our team type for this game."""
 36
 37    def __init__(self) -> None:
 38        self.score = 0
 39
 40
 41# ba_meta export bascenev1.GameActivity
 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)
205
206
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 'orchestraHit3'
325                        if streak > 2
326                        else 'orchestraHit2'
327                        if streak > 1
328                        else 'orchestraHit'
329                    ).play()
330            elif dist <= self._r2 + self._rfudge:
331                self._nodes[0].color = cdull
332                self._nodes[2].color = cdull
333                bs.animate_array(self._nodes[1], 'color', 3, keys, loop=True)
334                popupscale = 1.25
335                popupcolor = (1, 0.5, 0.2, 1)
336                points = 4
337                bs.getsound('bellMed').play()
338            else:
339                self._nodes[0].color = cdull
340                self._nodes[1].color = cdull
341                bs.animate_array(self._nodes[2], 'color', 3, keys, loop=True)
342                popupscale = 1.0
343                popupcolor = (0.8, 0.3, 0.3, 1)
344                points = 2
345                bs.getsound('bellLow').play()
346
347            # Award points/etc.. (technically should probably leave this up
348            # to the activity).
349            popupstr = '+' + str(points)
350
351            # If there's more than 1 player in the game, include their
352            # names and colors so they know who got the hit.
353            if len(activity.players) > 1:
354                popupcolor = bs.safecolor(player.color, target_intensity=0.75)
355                popupstr += ' ' + player.getname()
356            PopupText(
357                popupstr,
358                position=self._position,
359                color=popupcolor,
360                scale=popupscale,
361            ).autoretain()
362
363            # Give this player's team points and update the score-board.
364            player.team.score += points
365            assert isinstance(activity, TargetPracticeGame)
366            activity.update_scoreboard()
367
368            # Also give this individual player points
369            # (only applies in teams mode).
370            assert activity.stats is not None
371            activity.stats.player_scored(
372                player, points, showpoints=False, screenmessage=False
373            )
374
375            bs.animate_array(
376                self._nodes[0],
377                'size',
378                1,
379                {0.8: self._nodes[0].size, 1.0: [0.0]},
380            )
381            bs.animate_array(
382                self._nodes[1],
383                'size',
384                1,
385                {0.85: self._nodes[1].size, 1.05: [0.0]},
386            )
387            bs.animate_array(
388                self._nodes[2],
389                'size',
390                1,
391                {0.9: self._nodes[2].size, 1.1: [0.0]},
392            )
393            bs.timer(1.1, bs.Call(self.handlemessage, bs.DieMessage()))
394
395        return bullseye
class Player(bascenev1._player.Player[ForwardRef('Team')]):
28class Player(bs.Player['Team']):
29    """Our player type for this game."""
30
31    def __init__(self) -> None:
32        self.streak = 0

Our player type for this game.

streak
Inherited Members
bascenev1._player.Player
character
actor
color
highlight
on_expire
team
customdata
sessionplayer
node
position
exists
getname
is_alive
get_icon
assigninput
resetinput
class Team(bascenev1._team.Team[bascenev1lib.game.targetpractice.Player]):
35class Team(bs.Team[Player]):
36    """Our team type for this game."""
37
38    def __init__(self) -> None:
39        self.score = 0

Our team type for this game.

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

Game where players try to hit targets with bombs.

TargetPracticeGame(settings: dict)
68    def __init__(self, settings: dict):
69        super().__init__(settings)
70        self._scoreboard = Scoreboard()
71        self._targets: list[Target] = []
72        self._update_timer: bs.Timer | None = None
73        self._countdown: OnScreenCountdown | None = None
74        self._target_count = int(settings['Target Count'])
75        self._enable_impact_bombs = bool(settings['Enable Impact Bombs'])
76        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.Session]) -> list[str]:
55    @override
56    @classmethod
57    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
58        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.Session]) -> bool:
60    @override
61    @classmethod
62    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
63        # We support any teams or versus sessions.
64        return issubclass(sessiontype, bs.CoopSession) or issubclass(
65            sessiontype, bs.MultiTeamSession
66        )

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

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

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

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

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

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

General message handling; can be passed any message object.

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

Update the game scoreboard with current team values.

@override
def end_game(self) -> None:
200    @override
201    def end_game(self) -> None:
202        results = bs.GameResults()
203        for team in self.teams:
204            results.set_team_score(team, team.score)
205        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.

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

A target practice target.

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

Instantiates an Actor in the current bascenev1.Activity.

@override
def exists(self) -> bool:
265    @override
266    def exists(self) -> bool:
267        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:
269    @override
270    def handlemessage(self, msg: Any) -> Any:
271        if isinstance(msg, bs.DieMessage):
272            for node in self._nodes:
273                node.delete()
274            self._nodes = []
275        else:
276            super().handlemessage(msg)

General message handling; can be passed any message object.

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

Handle a bomb hit at the given position.

Inherited Members
bascenev1._actor.Actor
autoretain
on_expire
expired
is_alive
activity
getactivity
class Target.TargetHitMessage:
211    class TargetHitMessage:
212        """Inform an object a target was hit."""

Inform an object a target was hit.