bastd.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 7 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10import random 11from typing import TYPE_CHECKING 12 13import ba 14from bastd.actor.scoreboard import Scoreboard 15from bastd.actor.onscreencountdown import OnScreenCountdown 16from bastd.actor.bomb import Bomb 17from bastd.actor.popuptext import PopupText 18 19if TYPE_CHECKING: 20 from typing import Any, Sequence 21 from bastd.actor.bomb import Blast 22 23 24class Player(ba.Player['Team']): 25 """Our player type for this game.""" 26 27 def __init__(self) -> None: 28 self.streak = 0 29 30 31class Team(ba.Team[Player]): 32 """Our team type for this game.""" 33 34 def __init__(self) -> None: 35 self.score = 0 36 37 38# ba_meta export game 39class TargetPracticeGame(ba.TeamGameActivity[Player, Team]): 40 """Game where players try to hit targets with bombs.""" 41 42 name = 'Target Practice' 43 description = 'Bomb as many targets as you can.' 44 available_settings = [ 45 ba.IntSetting('Target Count', min_value=1, default=3), 46 ba.BoolSetting('Enable Impact Bombs', default=True), 47 ba.BoolSetting('Enable Triple Bombs', default=True), 48 ] 49 default_music = ba.MusicType.FORWARD_MARCH 50 51 @classmethod 52 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 53 return ['Doom Shroom'] 54 55 @classmethod 56 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 57 # We support any teams or versus sessions. 58 return issubclass(sessiontype, ba.CoopSession) or issubclass( 59 sessiontype, ba.MultiTeamSession 60 ) 61 62 def __init__(self, settings: dict): 63 super().__init__(settings) 64 self._scoreboard = Scoreboard() 65 self._targets: list[Target] = [] 66 self._update_timer: ba.Timer | None = None 67 self._countdown: OnScreenCountdown | None = None 68 self._target_count = int(settings['Target Count']) 69 self._enable_impact_bombs = bool(settings['Enable Impact Bombs']) 70 self._enable_triple_bombs = bool(settings['Enable Triple Bombs']) 71 72 def on_team_join(self, team: Team) -> None: 73 if self.has_begun(): 74 self.update_scoreboard() 75 76 def on_begin(self) -> None: 77 super().on_begin() 78 self.update_scoreboard() 79 80 # Number of targets is based on player count. 81 for i in range(self._target_count): 82 ba.timer(5.0 + i * 1.0, self._spawn_target) 83 84 self._update_timer = ba.Timer(1.0, self._update, repeat=True) 85 self._countdown = OnScreenCountdown(60, endcall=self.end_game) 86 ba.timer(4.0, self._countdown.start) 87 88 def spawn_player(self, player: Player) -> ba.Actor: 89 spawn_center = (0, 3, -5) 90 pos = ( 91 spawn_center[0] + random.uniform(-1.5, 1.5), 92 spawn_center[1], 93 spawn_center[2] + random.uniform(-1.5, 1.5), 94 ) 95 96 # Reset their streak. 97 player.streak = 0 98 spaz = self.spawn_player_spaz(player, position=pos) 99 100 # Give players permanent triple impact bombs and wire them up 101 # to tell us when they drop a bomb. 102 if self._enable_impact_bombs: 103 spaz.bomb_type = 'impact' 104 if self._enable_triple_bombs: 105 spaz.set_bomb_count(3) 106 spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb) 107 return spaz 108 109 def _spawn_target(self) -> None: 110 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(ba.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos)) 123 124 def get_min_dist_from_target(pnt: ba.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: ba.Actor, bomb: ba.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 # ba.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, ba.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 = ba.GameResults() 193 for team in self.teams: 194 results.set_team_score(team, team.score) 195 self.end(results) 196 197 198class Target(ba.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 = ba.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 = ba.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 = ba.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 = ba.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 ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]}) 251 ba.animate_array(loc2, 'size', 1, {0.05: [0.0], 0.25: [self._r2 * 2.0]}) 252 ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]}) 253 ba.playsound(ba.getsound('laserReverse')) 254 255 def exists(self) -> bool: 256 return bool(self._nodes) 257 258 def handlemessage(self, msg: Any) -> Any: 259 if isinstance(msg, ba.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: ba.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 = ba.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 ba.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 ba.playsound(ba.getsound('bellHigh')) 309 if streak > 0: 310 ba.playsound( 311 ba.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 ) 320 ) 321 elif dist <= self._r2 + self._rfudge: 322 self._nodes[0].color = cdull 323 self._nodes[2].color = cdull 324 ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True) 325 popupscale = 1.25 326 popupcolor = (1, 0.5, 0.2, 1) 327 points = 4 328 ba.playsound(ba.getsound('bellMed')) 329 else: 330 self._nodes[0].color = cdull 331 self._nodes[1].color = cdull 332 ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True) 333 popupscale = 1.0 334 popupcolor = (0.8, 0.3, 0.3, 1) 335 points = 2 336 ba.playsound(ba.getsound('bellLow')) 337 338 # Award points/etc.. (technically should probably leave this up 339 # to the activity). 340 popupstr = '+' + str(points) 341 342 # If there's more than 1 player in the game, include their 343 # names and colors so they know who got the hit. 344 if len(activity.players) > 1: 345 popupcolor = ba.safecolor(player.color, target_intensity=0.75) 346 popupstr += ' ' + player.getname() 347 PopupText( 348 popupstr, 349 position=self._position, 350 color=popupcolor, 351 scale=popupscale, 352 ).autoretain() 353 354 # Give this player's team points and update the score-board. 355 player.team.score += points 356 assert isinstance(activity, TargetPracticeGame) 357 activity.update_scoreboard() 358 359 # Also give this individual player points 360 # (only applies in teams mode). 361 assert activity.stats is not None 362 activity.stats.player_scored( 363 player, points, showpoints=False, screenmessage=False 364 ) 365 366 ba.animate_array( 367 self._nodes[0], 368 'size', 369 1, 370 {0.8: self._nodes[0].size, 1.0: [0.0]}, 371 ) 372 ba.animate_array( 373 self._nodes[1], 374 'size', 375 1, 376 {0.85: self._nodes[1].size, 1.05: [0.0]}, 377 ) 378 ba.animate_array( 379 self._nodes[2], 380 'size', 381 1, 382 {0.9: self._nodes[2].size, 1.1: [0.0]}, 383 ) 384 ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage())) 385 386 return bullseye
25class Player(ba.Player['Team']): 26 """Our player type for this game.""" 27 28 def __init__(self) -> None: 29 self.streak = 0
Our player type for this game.
Inherited Members
- ba._player.Player
- actor
- on_expire
- team
- customdata
- sessionplayer
- node
- position
- exists
- getname
- is_alive
- get_icon
- assigninput
- resetinput
32class Team(ba.Team[Player]): 33 """Our team type for this game.""" 34 35 def __init__(self) -> None: 36 self.score = 0
Our team type for this game.
Inherited Members
- ba._team.Team
- manual_init
- customdata
- on_expire
- sessionteam
40class TargetPracticeGame(ba.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 ba.IntSetting('Target Count', min_value=1, default=3), 47 ba.BoolSetting('Enable Impact Bombs', default=True), 48 ba.BoolSetting('Enable Triple Bombs', default=True), 49 ] 50 default_music = ba.MusicType.FORWARD_MARCH 51 52 @classmethod 53 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 54 return ['Doom Shroom'] 55 56 @classmethod 57 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 58 # We support any teams or versus sessions. 59 return issubclass(sessiontype, ba.CoopSession) or issubclass( 60 sessiontype, ba.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: ba.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 ba.timer(5.0 + i * 1.0, self._spawn_target) 84 85 self._update_timer = ba.Timer(1.0, self._update, repeat=True) 86 self._countdown = OnScreenCountdown(60, endcall=self.end_game) 87 ba.timer(4.0, self._countdown.start) 88 89 def spawn_player(self, player: Player) -> ba.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 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(ba.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos)) 124 125 def get_min_dist_from_target(pnt: ba.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: ba.Actor, bomb: ba.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 # ba.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, ba.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 = ba.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.
63 def __init__(self, settings: dict): 64 super().__init__(settings) 65 self._scoreboard = Scoreboard() 66 self._targets: list[Target] = [] 67 self._update_timer: ba.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'])
Instantiate the Activity.
52 @classmethod 53 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 54 return ['Doom Shroom']
Called by the default ba.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given ba.Session type.
56 @classmethod 57 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 58 # We support any teams or versus sessions. 59 return issubclass(sessiontype, ba.CoopSession) or issubclass( 60 sessiontype, ba.MultiTeamSession 61 )
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
Called when a new ba.Team joins the Activity.
(including the initial set of Teams)
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 ba.timer(5.0 + i * 1.0, self._spawn_target) 84 85 self._update_timer = ba.Timer(1.0, self._update, repeat=True) 86 self._countdown = OnScreenCountdown(60, endcall=self.end_game) 87 ba.timer(4.0, self._countdown.start)
Called once the previous ba.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.
89 def spawn_player(self, player: Player) -> ba.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
Spawn something for the provided ba.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, ba.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 = ba.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 ba.Activity.end() immediately.
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 (ba.GameActivity.setup_standard_time_limit()) will work with the game.
Inherited Members
- ba._teamgame.TeamGameActivity
- on_transition_in
- spawn_player_spaz
- end
- ba._gameactivity.GameActivity
- allow_pausing
- allow_kick_idle_players
- 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
- 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
- ba._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
- 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
- ba._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps
199class Target(ba.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 = ba.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 = ba.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 = ba.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 = ba.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 ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]}) 252 ba.animate_array(loc2, 'size', 1, {0.05: [0.0], 0.25: [self._r2 * 2.0]}) 253 ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]}) 254 ba.playsound(ba.getsound('laserReverse')) 255 256 def exists(self) -> bool: 257 return bool(self._nodes) 258 259 def handlemessage(self, msg: Any) -> Any: 260 if isinstance(msg, ba.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: ba.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 = ba.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 ba.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 ba.playsound(ba.getsound('bellHigh')) 310 if streak > 0: 311 ba.playsound( 312 ba.getsound( 313 'orchestraHit4' 314 if streak > 3 315 else 'orchestraHit3' 316 if streak > 2 317 else 'orchestraHit2' 318 if streak > 1 319 else 'orchestraHit' 320 ) 321 ) 322 elif dist <= self._r2 + self._rfudge: 323 self._nodes[0].color = cdull 324 self._nodes[2].color = cdull 325 ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True) 326 popupscale = 1.25 327 popupcolor = (1, 0.5, 0.2, 1) 328 points = 4 329 ba.playsound(ba.getsound('bellMed')) 330 else: 331 self._nodes[0].color = cdull 332 self._nodes[1].color = cdull 333 ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True) 334 popupscale = 1.0 335 popupcolor = (0.8, 0.3, 0.3, 1) 336 points = 2 337 ba.playsound(ba.getsound('bellLow')) 338 339 # Award points/etc.. (technically should probably leave this up 340 # to the activity). 341 popupstr = '+' + str(points) 342 343 # If there's more than 1 player in the game, include their 344 # names and colors so they know who got the hit. 345 if len(activity.players) > 1: 346 popupcolor = ba.safecolor(player.color, target_intensity=0.75) 347 popupstr += ' ' + player.getname() 348 PopupText( 349 popupstr, 350 position=self._position, 351 color=popupcolor, 352 scale=popupscale, 353 ).autoretain() 354 355 # Give this player's team points and update the score-board. 356 player.team.score += points 357 assert isinstance(activity, TargetPracticeGame) 358 activity.update_scoreboard() 359 360 # Also give this individual player points 361 # (only applies in teams mode). 362 assert activity.stats is not None 363 activity.stats.player_scored( 364 player, points, showpoints=False, screenmessage=False 365 ) 366 367 ba.animate_array( 368 self._nodes[0], 369 'size', 370 1, 371 {0.8: self._nodes[0].size, 1.0: [0.0]}, 372 ) 373 ba.animate_array( 374 self._nodes[1], 375 'size', 376 1, 377 {0.85: self._nodes[1].size, 1.05: [0.0]}, 378 ) 379 ba.animate_array( 380 self._nodes[2], 381 'size', 382 1, 383 {0.9: self._nodes[2].size, 1.1: [0.0]}, 384 ) 385 ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage())) 386 387 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 = ba.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 = ba.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 = ba.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 = ba.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 ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]}) 252 ba.animate_array(loc2, 'size', 1, {0.05: [0.0], 0.25: [self._r2 * 2.0]}) 253 ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]}) 254 ba.playsound(ba.getsound('laserReverse'))
Instantiates an Actor in the current ba.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 ba.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 ba.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, ba.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: ba.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 = ba.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 ba.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 ba.playsound(ba.getsound('bellHigh')) 310 if streak > 0: 311 ba.playsound( 312 ba.getsound( 313 'orchestraHit4' 314 if streak > 3 315 else 'orchestraHit3' 316 if streak > 2 317 else 'orchestraHit2' 318 if streak > 1 319 else 'orchestraHit' 320 ) 321 ) 322 elif dist <= self._r2 + self._rfudge: 323 self._nodes[0].color = cdull 324 self._nodes[2].color = cdull 325 ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True) 326 popupscale = 1.25 327 popupcolor = (1, 0.5, 0.2, 1) 328 points = 4 329 ba.playsound(ba.getsound('bellMed')) 330 else: 331 self._nodes[0].color = cdull 332 self._nodes[1].color = cdull 333 ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True) 334 popupscale = 1.0 335 popupcolor = (0.8, 0.3, 0.3, 1) 336 points = 2 337 ba.playsound(ba.getsound('bellLow')) 338 339 # Award points/etc.. (technically should probably leave this up 340 # to the activity). 341 popupstr = '+' + str(points) 342 343 # If there's more than 1 player in the game, include their 344 # names and colors so they know who got the hit. 345 if len(activity.players) > 1: 346 popupcolor = ba.safecolor(player.color, target_intensity=0.75) 347 popupstr += ' ' + player.getname() 348 PopupText( 349 popupstr, 350 position=self._position, 351 color=popupcolor, 352 scale=popupscale, 353 ).autoretain() 354 355 # Give this player's team points and update the score-board. 356 player.team.score += points 357 assert isinstance(activity, TargetPracticeGame) 358 activity.update_scoreboard() 359 360 # Also give this individual player points 361 # (only applies in teams mode). 362 assert activity.stats is not None 363 activity.stats.player_scored( 364 player, points, showpoints=False, screenmessage=False 365 ) 366 367 ba.animate_array( 368 self._nodes[0], 369 'size', 370 1, 371 {0.8: self._nodes[0].size, 1.0: [0.0]}, 372 ) 373 ba.animate_array( 374 self._nodes[1], 375 'size', 376 1, 377 {0.85: self._nodes[1].size, 1.05: [0.0]}, 378 ) 379 ba.animate_array( 380 self._nodes[2], 381 'size', 382 1, 383 {0.9: self._nodes[2].size, 1.1: [0.0]}, 384 ) 385 ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage())) 386 387 return bullseye
Handle a bomb hit at the given position.
Inherited Members
- ba._actor.Actor
- autoretain
- on_expire
- expired
- is_alive
- activity
- getactivity
Inform an object a target was hit.