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 bascenev1lib.actor.scoreboard import Scoreboard
 14from bascenev1lib.actor.onscreencountdown import OnScreenCountdown
 15from bascenev1lib.actor.bomb import Bomb
 16from bascenev1lib.actor.popuptext import PopupText
 17import bascenev1 as bs
 18
 19if TYPE_CHECKING:
 20    from typing import Any, Sequence
 21
 22    from bascenev1lib.actor.bomb import Blast
 23
 24
 25class Player(bs.Player['Team']):
 26    """Our player type for this game."""
 27
 28    def __init__(self) -> None:
 29        self.streak = 0
 30
 31
 32class Team(bs.Team[Player]):
 33    """Our team type for this game."""
 34
 35    def __init__(self) -> None:
 36        self.score = 0
 37
 38
 39# ba_meta export bascenev1.GameActivity
 40class TargetPracticeGame(bs.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        bs.IntSetting('Target Count', min_value=1, default=3),
 47        bs.BoolSetting('Enable Impact Bombs', default=True),
 48        bs.BoolSetting('Enable Triple Bombs', default=True),
 49    ]
 50    default_music = bs.MusicType.FORWARD_MARCH
 51
 52    @classmethod
 53    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
 54        return ['Doom Shroom']
 55
 56    @classmethod
 57    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 58        # We support any teams or versus sessions.
 59        return issubclass(sessiontype, bs.CoopSession) or issubclass(
 60            sessiontype, bs.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: bs.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            bs.timer(5.0 + i * 1.0, self._spawn_target)
 84
 85        self._update_timer = bs.Timer(1.0, self._update, repeat=True)
 86        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
 87        bs.timer(4.0, self._countdown.start)
 88
 89    def spawn_player(self, player: Player) -> bs.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        # 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(bs.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos))
123
124        def get_min_dist_from_target(pnt: bs.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: bs.Actor, bomb: bs.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        # bs.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, bs.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 = bs.GameResults()
193        for team in self.teams:
194            results.set_team_score(team, team.score)
195        self.end(results)
196
197
198class Target(bs.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 = bs.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 = bs.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 = bs.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 = bs.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        bs.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]})
251        bs.animate_array(loc2, 'size', 1, {0.05: [0.0], 0.25: [self._r2 * 2.0]})
252        bs.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]})
253        bs.getsound('laserReverse').play()
254
255    def exists(self) -> bool:
256        return bool(self._nodes)
257
258    def handlemessage(self, msg: Any) -> Any:
259        if isinstance(msg, bs.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: bs.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 = bs.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                bs.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                bs.getsound('bellHigh').play()
309                if streak > 0:
310                    bs.getsound(
311                        'orchestraHit4'
312                        if streak > 3
313                        else 'orchestraHit3'
314                        if streak > 2
315                        else 'orchestraHit2'
316                        if streak > 1
317                        else 'orchestraHit'
318                    ).play()
319            elif dist <= self._r2 + self._rfudge:
320                self._nodes[0].color = cdull
321                self._nodes[2].color = cdull
322                bs.animate_array(self._nodes[1], 'color', 3, keys, loop=True)
323                popupscale = 1.25
324                popupcolor = (1, 0.5, 0.2, 1)
325                points = 4
326                bs.getsound('bellMed').play()
327            else:
328                self._nodes[0].color = cdull
329                self._nodes[1].color = cdull
330                bs.animate_array(self._nodes[2], 'color', 3, keys, loop=True)
331                popupscale = 1.0
332                popupcolor = (0.8, 0.3, 0.3, 1)
333                points = 2
334                bs.getsound('bellLow').play()
335
336            # Award points/etc.. (technically should probably leave this up
337            # to the activity).
338            popupstr = '+' + str(points)
339
340            # If there's more than 1 player in the game, include their
341            # names and colors so they know who got the hit.
342            if len(activity.players) > 1:
343                popupcolor = bs.safecolor(player.color, target_intensity=0.75)
344                popupstr += ' ' + player.getname()
345            PopupText(
346                popupstr,
347                position=self._position,
348                color=popupcolor,
349                scale=popupscale,
350            ).autoretain()
351
352            # Give this player's team points and update the score-board.
353            player.team.score += points
354            assert isinstance(activity, TargetPracticeGame)
355            activity.update_scoreboard()
356
357            # Also give this individual player points
358            # (only applies in teams mode).
359            assert activity.stats is not None
360            activity.stats.player_scored(
361                player, points, showpoints=False, screenmessage=False
362            )
363
364            bs.animate_array(
365                self._nodes[0],
366                'size',
367                1,
368                {0.8: self._nodes[0].size, 1.0: [0.0]},
369            )
370            bs.animate_array(
371                self._nodes[1],
372                'size',
373                1,
374                {0.85: self._nodes[1].size, 1.05: [0.0]},
375            )
376            bs.animate_array(
377                self._nodes[2],
378                'size',
379                1,
380                {0.9: self._nodes[2].size, 1.1: [0.0]},
381            )
382            bs.timer(1.1, bs.Call(self.handlemessage, bs.DieMessage()))
383
384        return bullseye
class Player(bascenev1._player.Player[ForwardRef('Team')]):
26class Player(bs.Player['Team']):
27    """Our player type for this game."""
28
29    def __init__(self) -> None:
30        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]):
33class Team(bs.Team[Player]):
34    """Our team type for this game."""
35
36    def __init__(self) -> None:
37        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]):
 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    @classmethod
 54    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
 55        return ['Doom Shroom']
 56
 57    @classmethod
 58    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 59        # We support any teams or versus sessions.
 60        return issubclass(sessiontype, bs.CoopSession) or issubclass(
 61            sessiontype, bs.MultiTeamSession
 62        )
 63
 64    def __init__(self, settings: dict):
 65        super().__init__(settings)
 66        self._scoreboard = Scoreboard()
 67        self._targets: list[Target] = []
 68        self._update_timer: bs.Timer | None = None
 69        self._countdown: OnScreenCountdown | None = None
 70        self._target_count = int(settings['Target Count'])
 71        self._enable_impact_bombs = bool(settings['Enable Impact Bombs'])
 72        self._enable_triple_bombs = bool(settings['Enable Triple Bombs'])
 73
 74    def on_team_join(self, team: Team) -> None:
 75        if self.has_begun():
 76            self.update_scoreboard()
 77
 78    def on_begin(self) -> None:
 79        super().on_begin()
 80        self.update_scoreboard()
 81
 82        # Number of targets is based on player count.
 83        for i in range(self._target_count):
 84            bs.timer(5.0 + i * 1.0, self._spawn_target)
 85
 86        self._update_timer = bs.Timer(1.0, self._update, repeat=True)
 87        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
 88        bs.timer(4.0, self._countdown.start)
 89
 90    def spawn_player(self, player: Player) -> bs.Actor:
 91        spawn_center = (0, 3, -5)
 92        pos = (
 93            spawn_center[0] + random.uniform(-1.5, 1.5),
 94            spawn_center[1],
 95            spawn_center[2] + random.uniform(-1.5, 1.5),
 96        )
 97
 98        # Reset their streak.
 99        player.streak = 0
100        spaz = self.spawn_player_spaz(player, position=pos)
101
102        # Give players permanent triple impact bombs and wire them up
103        # to tell us when they drop a bomb.
104        if self._enable_impact_bombs:
105            spaz.bomb_type = 'impact'
106        if self._enable_triple_bombs:
107            spaz.set_bomb_count(3)
108        spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb)
109        return spaz
110
111    def _spawn_target(self) -> None:
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(bs.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos))
124
125        def get_min_dist_from_target(pnt: bs.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: bs.Actor, bomb: bs.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        # bs.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, bs.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 = bs.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)
64    def __init__(self, settings: dict):
65        super().__init__(settings)
66        self._scoreboard = Scoreboard()
67        self._targets: list[Target] = []
68        self._update_timer: bs.Timer | None = None
69        self._countdown: OnScreenCountdown | None = None
70        self._target_count = int(settings['Target Count'])
71        self._enable_impact_bombs = bool(settings['Enable Impact Bombs'])
72        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'>
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1._session.Session]) -> list[str]:
53    @classmethod
54    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
55        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.

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

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

def on_team_join(self, team: Team) -> None:
74    def on_team_join(self, team: Team) -> None:
75        if self.has_begun():
76            self.update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

def on_begin(self) -> None:
78    def on_begin(self) -> None:
79        super().on_begin()
80        self.update_scoreboard()
81
82        # Number of targets is based on player count.
83        for i in range(self._target_count):
84            bs.timer(5.0 + i * 1.0, self._spawn_target)
85
86        self._update_timer = bs.Timer(1.0, self._update, repeat=True)
87        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
88        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.

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

Spawn something for the provided bascenev1.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, bs.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 = bs.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 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):
199class Target(bs.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 = bs.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 = bs.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 = bs.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 = bs.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        bs.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]})
252        bs.animate_array(loc2, 'size', 1, {0.05: [0.0], 0.25: [self._r2 * 2.0]})
253        bs.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]})
254        bs.getsound('laserReverse').play()
255
256    def exists(self) -> bool:
257        return bool(self._nodes)
258
259    def handlemessage(self, msg: Any) -> Any:
260        if isinstance(msg, bs.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: bs.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 = bs.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                bs.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                bs.getsound('bellHigh').play()
310                if streak > 0:
311                    bs.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                    ).play()
320            elif dist <= self._r2 + self._rfudge:
321                self._nodes[0].color = cdull
322                self._nodes[2].color = cdull
323                bs.animate_array(self._nodes[1], 'color', 3, keys, loop=True)
324                popupscale = 1.25
325                popupcolor = (1, 0.5, 0.2, 1)
326                points = 4
327                bs.getsound('bellMed').play()
328            else:
329                self._nodes[0].color = cdull
330                self._nodes[1].color = cdull
331                bs.animate_array(self._nodes[2], 'color', 3, keys, loop=True)
332                popupscale = 1.0
333                popupcolor = (0.8, 0.3, 0.3, 1)
334                points = 2
335                bs.getsound('bellLow').play()
336
337            # Award points/etc.. (technically should probably leave this up
338            # to the activity).
339            popupstr = '+' + str(points)
340
341            # If there's more than 1 player in the game, include their
342            # names and colors so they know who got the hit.
343            if len(activity.players) > 1:
344                popupcolor = bs.safecolor(player.color, target_intensity=0.75)
345                popupstr += ' ' + player.getname()
346            PopupText(
347                popupstr,
348                position=self._position,
349                color=popupcolor,
350                scale=popupscale,
351            ).autoretain()
352
353            # Give this player's team points and update the score-board.
354            player.team.score += points
355            assert isinstance(activity, TargetPracticeGame)
356            activity.update_scoreboard()
357
358            # Also give this individual player points
359            # (only applies in teams mode).
360            assert activity.stats is not None
361            activity.stats.player_scored(
362                player, points, showpoints=False, screenmessage=False
363            )
364
365            bs.animate_array(
366                self._nodes[0],
367                'size',
368                1,
369                {0.8: self._nodes[0].size, 1.0: [0.0]},
370            )
371            bs.animate_array(
372                self._nodes[1],
373                'size',
374                1,
375                {0.85: self._nodes[1].size, 1.05: [0.0]},
376            )
377            bs.animate_array(
378                self._nodes[2],
379                'size',
380                1,
381                {0.9: self._nodes[2].size, 1.1: [0.0]},
382            )
383            bs.timer(1.1, bs.Call(self.handlemessage, bs.DieMessage()))
384
385        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 = bs.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 = bs.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 = bs.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 = bs.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        bs.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]})
252        bs.animate_array(loc2, 'size', 1, {0.05: [0.0], 0.25: [self._r2 * 2.0]})
253        bs.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]})
254        bs.getsound('laserReverse').play()

Instantiates an Actor in the current bascenev1.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 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.

def handlemessage(self, msg: Any) -> Any:
259    def handlemessage(self, msg: Any) -> Any:
260        if isinstance(msg, bs.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: _babase.Vec3) -> float:
267    def get_dist_from_point(self, pos: bs.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: 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 = bs.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                bs.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                bs.getsound('bellHigh').play()
310                if streak > 0:
311                    bs.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                    ).play()
320            elif dist <= self._r2 + self._rfudge:
321                self._nodes[0].color = cdull
322                self._nodes[2].color = cdull
323                bs.animate_array(self._nodes[1], 'color', 3, keys, loop=True)
324                popupscale = 1.25
325                popupcolor = (1, 0.5, 0.2, 1)
326                points = 4
327                bs.getsound('bellMed').play()
328            else:
329                self._nodes[0].color = cdull
330                self._nodes[1].color = cdull
331                bs.animate_array(self._nodes[2], 'color', 3, keys, loop=True)
332                popupscale = 1.0
333                popupcolor = (0.8, 0.3, 0.3, 1)
334                points = 2
335                bs.getsound('bellLow').play()
336
337            # Award points/etc.. (technically should probably leave this up
338            # to the activity).
339            popupstr = '+' + str(points)
340
341            # If there's more than 1 player in the game, include their
342            # names and colors so they know who got the hit.
343            if len(activity.players) > 1:
344                popupcolor = bs.safecolor(player.color, target_intensity=0.75)
345                popupstr += ' ' + player.getname()
346            PopupText(
347                popupstr,
348                position=self._position,
349                color=popupcolor,
350                scale=popupscale,
351            ).autoretain()
352
353            # Give this player's team points and update the score-board.
354            player.team.score += points
355            assert isinstance(activity, TargetPracticeGame)
356            activity.update_scoreboard()
357
358            # Also give this individual player points
359            # (only applies in teams mode).
360            assert activity.stats is not None
361            activity.stats.player_scored(
362                player, points, showpoints=False, screenmessage=False
363            )
364
365            bs.animate_array(
366                self._nodes[0],
367                'size',
368                1,
369                {0.8: self._nodes[0].size, 1.0: [0.0]},
370            )
371            bs.animate_array(
372                self._nodes[1],
373                'size',
374                1,
375                {0.85: self._nodes[1].size, 1.05: [0.0]},
376            )
377            bs.animate_array(
378                self._nodes[2],
379                'size',
380                1,
381                {0.9: self._nodes[2].size, 1.1: [0.0]},
382            )
383            bs.timer(1.1, bs.Call(self.handlemessage, bs.DieMessage()))
384
385        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:
202    class TargetHitMessage:
203        """Inform an object a target was hit."""

Inform an object a target was hit.