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
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.
Inherited Members
- bascenev1._player.Player
- character
- actor
- color
- highlight
- on_expire
- team
- customdata
- sessionplayer
- node
- position
- exists
- getname
- is_alive
- get_icon
- assigninput
- resetinput
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.
Inherited Members
- bascenev1._team.Team
- players
- id
- name
- color
- manual_init
- customdata
- on_expire
- sessionteam
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.
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.
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.
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.
Called when a new bascenev1.Team joins the Activity.
(including the initial set of Teams)
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.
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().
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.
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.
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
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.
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.
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.
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.
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.
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
Inform an object a target was hit.