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
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.
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.
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.
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.
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.
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.
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)
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.
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().
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.
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.
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.
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.
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.
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.
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.
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.
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.
Inform an object a target was hit.