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) 59 or issubclass(sessiontype, ba.MultiTeamSession)) 60 61 def __init__(self, settings: dict): 62 super().__init__(settings) 63 self._scoreboard = Scoreboard() 64 self._targets: list[Target] = [] 65 self._update_timer: ba.Timer | None = None 66 self._countdown: OnScreenCountdown | None = None 67 self._target_count = int(settings['Target Count']) 68 self._enable_impact_bombs = bool(settings['Enable Impact Bombs']) 69 self._enable_triple_bombs = bool(settings['Enable Triple Bombs']) 70 71 def on_team_join(self, team: Team) -> None: 72 if self.has_begun(): 73 self.update_scoreboard() 74 75 def on_begin(self) -> None: 76 super().on_begin() 77 self.update_scoreboard() 78 79 # Number of targets is based on player count. 80 for i in range(self._target_count): 81 ba.timer(5.0 + i * 1.0, self._spawn_target) 82 83 self._update_timer = ba.Timer(1.0, self._update, repeat=True) 84 self._countdown = OnScreenCountdown(60, endcall=self.end_game) 85 ba.timer(4.0, self._countdown.start) 86 87 def spawn_player(self, player: Player) -> ba.Actor: 88 spawn_center = (0, 3, -5) 89 pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], 90 spawn_center[2] + random.uniform(-1.5, 1.5)) 91 92 # Reset their streak. 93 player.streak = 0 94 spaz = self.spawn_player_spaz(player, position=pos) 95 96 # Give players permanent triple impact bombs and wire them up 97 # to tell us when they drop a bomb. 98 if self._enable_impact_bombs: 99 spaz.bomb_type = 'impact' 100 if self._enable_triple_bombs: 101 spaz.set_bomb_count(3) 102 spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb) 103 return spaz 104 105 def _spawn_target(self) -> None: 106 107 # Generate a few random points; we'll use whichever one is farthest 108 # from our existing targets (don't want overlapping targets). 109 points = [] 110 111 for _i in range(4): 112 # Calc a random point within a circle. 113 while True: 114 xpos = random.uniform(-1.0, 1.0) 115 ypos = random.uniform(-1.0, 1.0) 116 if xpos * xpos + ypos * ypos < 1.0: 117 break 118 points.append(ba.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos)) 119 120 def get_min_dist_from_target(pnt: ba.Vec3) -> float: 121 return min((t.get_dist_from_point(pnt) for t in self._targets)) 122 123 # If we have existing targets, use the point with the highest 124 # min-distance-from-targets. 125 if self._targets: 126 point = max(points, key=get_min_dist_from_target) 127 else: 128 point = points[0] 129 130 self._targets.append(Target(position=point)) 131 132 def _on_spaz_dropped_bomb(self, spaz: ba.Actor, bomb: ba.Actor) -> None: 133 del spaz # Unused. 134 135 # Wire up this bomb to inform us when it blows up. 136 assert isinstance(bomb, Bomb) 137 bomb.add_explode_callback(self._on_bomb_exploded) 138 139 def _on_bomb_exploded(self, bomb: Bomb, blast: Blast) -> None: 140 assert blast.node 141 pos = blast.node.position 142 143 # Debugging: throw a locator down where we landed. 144 # ba.newnode('locator', attrs={'position':blast.node.position}) 145 146 # Feed the explosion point to all our targets and get points in return. 147 # Note: we operate on a copy of self._targets since the list may change 148 # under us if we hit stuff (don't wanna get points for new targets). 149 player = bomb.get_source_player(Player) 150 if not player: 151 # It's possible the player left after throwing the bomb. 152 return 153 154 bullseye = any( 155 target.do_hit_at_position(pos, player) 156 for target in list(self._targets)) 157 if bullseye: 158 player.streak += 1 159 else: 160 player.streak = 0 161 162 def _update(self) -> None: 163 """Misc. periodic updating.""" 164 # Clear out targets that have died. 165 self._targets = [t for t in self._targets if t] 166 167 def handlemessage(self, msg: Any) -> Any: 168 # When players die, respawn them. 169 if isinstance(msg, ba.PlayerDiedMessage): 170 super().handlemessage(msg) # Do standard stuff. 171 player = msg.getplayer(Player) 172 assert player is not None 173 self.respawn_player(player) # Kick off a respawn. 174 elif isinstance(msg, Target.TargetHitMessage): 175 # A target is telling us it was hit and will die soon.. 176 # ..so make another one. 177 self._spawn_target() 178 else: 179 super().handlemessage(msg) 180 181 def update_scoreboard(self) -> None: 182 """Update the game scoreboard with current team values.""" 183 for team in self.teams: 184 self._scoreboard.set_team_value(team, team.score) 185 186 def end_game(self) -> None: 187 results = ba.GameResults() 188 for team in self.teams: 189 results.set_team_score(team, team.score) 190 self.end(results) 191 192 193class Target(ba.Actor): 194 """A target practice target.""" 195 196 class TargetHitMessage: 197 """Inform an object a target was hit.""" 198 199 def __init__(self, position: Sequence[float]): 200 self._r1 = 0.45 201 self._r2 = 1.1 202 self._r3 = 2.0 203 self._rfudge = 0.15 204 super().__init__() 205 self._position = ba.Vec3(position) 206 self._hit = False 207 208 # It can be handy to test with this on to make sure the projection 209 # isn't too far off from the actual object. 210 show_in_space = False 211 loc1 = ba.newnode('locator', 212 attrs={ 213 'shape': 'circle', 214 'position': position, 215 'color': (0, 1, 0), 216 'opacity': 0.5, 217 'draw_beauty': show_in_space, 218 'additive': True 219 }) 220 loc2 = ba.newnode('locator', 221 attrs={ 222 'shape': 'circleOutline', 223 'position': position, 224 'color': (0, 1, 0), 225 'opacity': 0.3, 226 'draw_beauty': False, 227 'additive': True 228 }) 229 loc3 = ba.newnode('locator', 230 attrs={ 231 'shape': 'circleOutline', 232 'position': position, 233 'color': (0, 1, 0), 234 'opacity': 0.1, 235 'draw_beauty': False, 236 'additive': True 237 }) 238 self._nodes = [loc1, loc2, loc3] 239 ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]}) 240 ba.animate_array(loc2, 'size', 1, { 241 0.05: [0.0], 242 0.25: [self._r2 * 2.0] 243 }) 244 ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]}) 245 ba.playsound(ba.getsound('laserReverse')) 246 247 def exists(self) -> bool: 248 return bool(self._nodes) 249 250 def handlemessage(self, msg: Any) -> Any: 251 if isinstance(msg, ba.DieMessage): 252 for node in self._nodes: 253 node.delete() 254 self._nodes = [] 255 else: 256 super().handlemessage(msg) 257 258 def get_dist_from_point(self, pos: ba.Vec3) -> float: 259 """Given a point, returns distance squared from it.""" 260 return (pos - self._position).length() 261 262 def do_hit_at_position(self, pos: Sequence[float], player: Player) -> bool: 263 """Handle a bomb hit at the given position.""" 264 # pylint: disable=too-many-statements 265 activity = self.activity 266 267 # Ignore hits if the game is over or if we've already been hit 268 if activity.has_ended() or self._hit or not self._nodes: 269 return False 270 271 diff = (ba.Vec3(pos) - self._position) 272 273 # Disregard Y difference. Our target point probably isn't exactly 274 # on the ground anyway. 275 diff[1] = 0.0 276 dist = diff.length() 277 278 bullseye = False 279 if dist <= self._r3 + self._rfudge: 280 # Inform our activity that we were hit 281 self._hit = True 282 activity.handlemessage(self.TargetHitMessage()) 283 keys: dict[float, Sequence[float]] = { 284 0.0: (1.0, 0.0, 0.0), 285 0.049: (1.0, 0.0, 0.0), 286 0.05: (1.0, 1.0, 1.0), 287 0.1: (0.0, 1.0, 0.0) 288 } 289 cdull = (0.3, 0.3, 0.3) 290 popupcolor: Sequence[float] 291 if dist <= self._r1 + self._rfudge: 292 bullseye = True 293 self._nodes[1].color = cdull 294 self._nodes[2].color = cdull 295 ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True) 296 popupscale = 1.8 297 popupcolor = (1, 1, 0, 1) 298 streak = player.streak 299 points = 10 + min(20, streak * 2) 300 ba.playsound(ba.getsound('bellHigh')) 301 if streak > 0: 302 ba.playsound( 303 ba.getsound( 304 'orchestraHit4' if streak > 3 else 305 'orchestraHit3' if streak > 2 else 306 'orchestraHit2' if streak > 1 else 'orchestraHit')) 307 elif dist <= self._r2 + self._rfudge: 308 self._nodes[0].color = cdull 309 self._nodes[2].color = cdull 310 ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True) 311 popupscale = 1.25 312 popupcolor = (1, 0.5, 0.2, 1) 313 points = 4 314 ba.playsound(ba.getsound('bellMed')) 315 else: 316 self._nodes[0].color = cdull 317 self._nodes[1].color = cdull 318 ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True) 319 popupscale = 1.0 320 popupcolor = (0.8, 0.3, 0.3, 1) 321 points = 2 322 ba.playsound(ba.getsound('bellLow')) 323 324 # Award points/etc.. (technically should probably leave this up 325 # to the activity). 326 popupstr = '+' + str(points) 327 328 # If there's more than 1 player in the game, include their 329 # names and colors so they know who got the hit. 330 if len(activity.players) > 1: 331 popupcolor = ba.safecolor(player.color, target_intensity=0.75) 332 popupstr += ' ' + player.getname() 333 PopupText(popupstr, 334 position=self._position, 335 color=popupcolor, 336 scale=popupscale).autoretain() 337 338 # Give this player's team points and update the score-board. 339 player.team.score += points 340 assert isinstance(activity, TargetPracticeGame) 341 activity.update_scoreboard() 342 343 # Also give this individual player points 344 # (only applies in teams mode). 345 assert activity.stats is not None 346 activity.stats.player_scored(player, 347 points, 348 showpoints=False, 349 screenmessage=False) 350 351 ba.animate_array(self._nodes[0], 'size', 1, { 352 0.8: self._nodes[0].size, 353 1.0: [0.0] 354 }) 355 ba.animate_array(self._nodes[1], 'size', 1, { 356 0.85: self._nodes[1].size, 357 1.05: [0.0] 358 }) 359 ba.animate_array(self._nodes[2], 'size', 1, { 360 0.9: self._nodes[2].size, 361 1.1: [0.0] 362 }) 363 ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage())) 364 365 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
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) 60 or issubclass(sessiontype, ba.MultiTeamSession)) 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 = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], 91 spawn_center[2] + random.uniform(-1.5, 1.5)) 92 93 # Reset their streak. 94 player.streak = 0 95 spaz = self.spawn_player_spaz(player, position=pos) 96 97 # Give players permanent triple impact bombs and wire them up 98 # to tell us when they drop a bomb. 99 if self._enable_impact_bombs: 100 spaz.bomb_type = 'impact' 101 if self._enable_triple_bombs: 102 spaz.set_bomb_count(3) 103 spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb) 104 return spaz 105 106 def _spawn_target(self) -> None: 107 108 # Generate a few random points; we'll use whichever one is farthest 109 # from our existing targets (don't want overlapping targets). 110 points = [] 111 112 for _i in range(4): 113 # Calc a random point within a circle. 114 while True: 115 xpos = random.uniform(-1.0, 1.0) 116 ypos = random.uniform(-1.0, 1.0) 117 if xpos * xpos + ypos * ypos < 1.0: 118 break 119 points.append(ba.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos)) 120 121 def get_min_dist_from_target(pnt: ba.Vec3) -> float: 122 return min((t.get_dist_from_point(pnt) for t in self._targets)) 123 124 # If we have existing targets, use the point with the highest 125 # min-distance-from-targets. 126 if self._targets: 127 point = max(points, key=get_min_dist_from_target) 128 else: 129 point = points[0] 130 131 self._targets.append(Target(position=point)) 132 133 def _on_spaz_dropped_bomb(self, spaz: ba.Actor, bomb: ba.Actor) -> None: 134 del spaz # Unused. 135 136 # Wire up this bomb to inform us when it blows up. 137 assert isinstance(bomb, Bomb) 138 bomb.add_explode_callback(self._on_bomb_exploded) 139 140 def _on_bomb_exploded(self, bomb: Bomb, blast: Blast) -> None: 141 assert blast.node 142 pos = blast.node.position 143 144 # Debugging: throw a locator down where we landed. 145 # ba.newnode('locator', attrs={'position':blast.node.position}) 146 147 # Feed the explosion point to all our targets and get points in return. 148 # Note: we operate on a copy of self._targets since the list may change 149 # under us if we hit stuff (don't wanna get points for new targets). 150 player = bomb.get_source_player(Player) 151 if not player: 152 # It's possible the player left after throwing the bomb. 153 return 154 155 bullseye = any( 156 target.do_hit_at_position(pos, player) 157 for target in list(self._targets)) 158 if bullseye: 159 player.streak += 1 160 else: 161 player.streak = 0 162 163 def _update(self) -> None: 164 """Misc. periodic updating.""" 165 # Clear out targets that have died. 166 self._targets = [t for t in self._targets if t] 167 168 def handlemessage(self, msg: Any) -> Any: 169 # When players die, respawn them. 170 if isinstance(msg, ba.PlayerDiedMessage): 171 super().handlemessage(msg) # Do standard stuff. 172 player = msg.getplayer(Player) 173 assert player is not None 174 self.respawn_player(player) # Kick off a respawn. 175 elif isinstance(msg, Target.TargetHitMessage): 176 # A target is telling us it was hit and will die soon.. 177 # ..so make another one. 178 self._spawn_target() 179 else: 180 super().handlemessage(msg) 181 182 def update_scoreboard(self) -> None: 183 """Update the game scoreboard with current team values.""" 184 for team in self.teams: 185 self._scoreboard.set_team_value(team, team.score) 186 187 def end_game(self) -> None: 188 results = ba.GameResults() 189 for team in self.teams: 190 results.set_team_score(team, team.score) 191 self.end(results)
Game where players try to hit targets with bombs.
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'])
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) 60 or issubclass(sessiontype, ba.MultiTeamSession))
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)
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)
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.
88 def spawn_player(self, player: Player) -> ba.Actor: 89 spawn_center = (0, 3, -5) 90 pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], 91 spawn_center[2] + random.uniform(-1.5, 1.5)) 92 93 # Reset their streak. 94 player.streak = 0 95 spaz = self.spawn_player_spaz(player, position=pos) 96 97 # Give players permanent triple impact bombs and wire them up 98 # to tell us when they drop a bomb. 99 if self._enable_impact_bombs: 100 spaz.bomb_type = 'impact' 101 if self._enable_triple_bombs: 102 spaz.set_bomb_count(3) 103 spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb) 104 return spaz
Spawn something for the provided ba.Player.
The default implementation simply calls spawn_player_spaz().
168 def handlemessage(self, msg: Any) -> Any: 169 # When players die, respawn them. 170 if isinstance(msg, ba.PlayerDiedMessage): 171 super().handlemessage(msg) # Do standard stuff. 172 player = msg.getplayer(Player) 173 assert player is not None 174 self.respawn_player(player) # Kick off a respawn. 175 elif isinstance(msg, Target.TargetHitMessage): 176 # A target is telling us it was hit and will die soon.. 177 # ..so make another one. 178 self._spawn_target() 179 else: 180 super().handlemessage(msg)
General message handling; can be passed any message object.
182 def update_scoreboard(self) -> None: 183 """Update the game scoreboard with current team values.""" 184 for team in self.teams: 185 self._scoreboard.set_team_value(team, team.score)
Update the game scoreboard with current team values.
187 def end_game(self) -> None: 188 results = ba.GameResults() 189 for team in self.teams: 190 results.set_team_score(team, team.score) 191 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
- 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
- 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
194class Target(ba.Actor): 195 """A target practice target.""" 196 197 class TargetHitMessage: 198 """Inform an object a target was hit.""" 199 200 def __init__(self, position: Sequence[float]): 201 self._r1 = 0.45 202 self._r2 = 1.1 203 self._r3 = 2.0 204 self._rfudge = 0.15 205 super().__init__() 206 self._position = ba.Vec3(position) 207 self._hit = False 208 209 # It can be handy to test with this on to make sure the projection 210 # isn't too far off from the actual object. 211 show_in_space = False 212 loc1 = ba.newnode('locator', 213 attrs={ 214 'shape': 'circle', 215 'position': position, 216 'color': (0, 1, 0), 217 'opacity': 0.5, 218 'draw_beauty': show_in_space, 219 'additive': True 220 }) 221 loc2 = ba.newnode('locator', 222 attrs={ 223 'shape': 'circleOutline', 224 'position': position, 225 'color': (0, 1, 0), 226 'opacity': 0.3, 227 'draw_beauty': False, 228 'additive': True 229 }) 230 loc3 = ba.newnode('locator', 231 attrs={ 232 'shape': 'circleOutline', 233 'position': position, 234 'color': (0, 1, 0), 235 'opacity': 0.1, 236 'draw_beauty': False, 237 'additive': True 238 }) 239 self._nodes = [loc1, loc2, loc3] 240 ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]}) 241 ba.animate_array(loc2, 'size', 1, { 242 0.05: [0.0], 243 0.25: [self._r2 * 2.0] 244 }) 245 ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]}) 246 ba.playsound(ba.getsound('laserReverse')) 247 248 def exists(self) -> bool: 249 return bool(self._nodes) 250 251 def handlemessage(self, msg: Any) -> Any: 252 if isinstance(msg, ba.DieMessage): 253 for node in self._nodes: 254 node.delete() 255 self._nodes = [] 256 else: 257 super().handlemessage(msg) 258 259 def get_dist_from_point(self, pos: ba.Vec3) -> float: 260 """Given a point, returns distance squared from it.""" 261 return (pos - self._position).length() 262 263 def do_hit_at_position(self, pos: Sequence[float], player: Player) -> bool: 264 """Handle a bomb hit at the given position.""" 265 # pylint: disable=too-many-statements 266 activity = self.activity 267 268 # Ignore hits if the game is over or if we've already been hit 269 if activity.has_ended() or self._hit or not self._nodes: 270 return False 271 272 diff = (ba.Vec3(pos) - self._position) 273 274 # Disregard Y difference. Our target point probably isn't exactly 275 # on the ground anyway. 276 diff[1] = 0.0 277 dist = diff.length() 278 279 bullseye = False 280 if dist <= self._r3 + self._rfudge: 281 # Inform our activity that we were hit 282 self._hit = True 283 activity.handlemessage(self.TargetHitMessage()) 284 keys: dict[float, Sequence[float]] = { 285 0.0: (1.0, 0.0, 0.0), 286 0.049: (1.0, 0.0, 0.0), 287 0.05: (1.0, 1.0, 1.0), 288 0.1: (0.0, 1.0, 0.0) 289 } 290 cdull = (0.3, 0.3, 0.3) 291 popupcolor: Sequence[float] 292 if dist <= self._r1 + self._rfudge: 293 bullseye = True 294 self._nodes[1].color = cdull 295 self._nodes[2].color = cdull 296 ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True) 297 popupscale = 1.8 298 popupcolor = (1, 1, 0, 1) 299 streak = player.streak 300 points = 10 + min(20, streak * 2) 301 ba.playsound(ba.getsound('bellHigh')) 302 if streak > 0: 303 ba.playsound( 304 ba.getsound( 305 'orchestraHit4' if streak > 3 else 306 'orchestraHit3' if streak > 2 else 307 'orchestraHit2' if streak > 1 else 'orchestraHit')) 308 elif dist <= self._r2 + self._rfudge: 309 self._nodes[0].color = cdull 310 self._nodes[2].color = cdull 311 ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True) 312 popupscale = 1.25 313 popupcolor = (1, 0.5, 0.2, 1) 314 points = 4 315 ba.playsound(ba.getsound('bellMed')) 316 else: 317 self._nodes[0].color = cdull 318 self._nodes[1].color = cdull 319 ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True) 320 popupscale = 1.0 321 popupcolor = (0.8, 0.3, 0.3, 1) 322 points = 2 323 ba.playsound(ba.getsound('bellLow')) 324 325 # Award points/etc.. (technically should probably leave this up 326 # to the activity). 327 popupstr = '+' + str(points) 328 329 # If there's more than 1 player in the game, include their 330 # names and colors so they know who got the hit. 331 if len(activity.players) > 1: 332 popupcolor = ba.safecolor(player.color, target_intensity=0.75) 333 popupstr += ' ' + player.getname() 334 PopupText(popupstr, 335 position=self._position, 336 color=popupcolor, 337 scale=popupscale).autoretain() 338 339 # Give this player's team points and update the score-board. 340 player.team.score += points 341 assert isinstance(activity, TargetPracticeGame) 342 activity.update_scoreboard() 343 344 # Also give this individual player points 345 # (only applies in teams mode). 346 assert activity.stats is not None 347 activity.stats.player_scored(player, 348 points, 349 showpoints=False, 350 screenmessage=False) 351 352 ba.animate_array(self._nodes[0], 'size', 1, { 353 0.8: self._nodes[0].size, 354 1.0: [0.0] 355 }) 356 ba.animate_array(self._nodes[1], 'size', 1, { 357 0.85: self._nodes[1].size, 358 1.05: [0.0] 359 }) 360 ba.animate_array(self._nodes[2], 'size', 1, { 361 0.9: self._nodes[2].size, 362 1.1: [0.0] 363 }) 364 ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage())) 365 366 return bullseye
A target practice target.
200 def __init__(self, position: Sequence[float]): 201 self._r1 = 0.45 202 self._r2 = 1.1 203 self._r3 = 2.0 204 self._rfudge = 0.15 205 super().__init__() 206 self._position = ba.Vec3(position) 207 self._hit = False 208 209 # It can be handy to test with this on to make sure the projection 210 # isn't too far off from the actual object. 211 show_in_space = False 212 loc1 = ba.newnode('locator', 213 attrs={ 214 'shape': 'circle', 215 'position': position, 216 'color': (0, 1, 0), 217 'opacity': 0.5, 218 'draw_beauty': show_in_space, 219 'additive': True 220 }) 221 loc2 = ba.newnode('locator', 222 attrs={ 223 'shape': 'circleOutline', 224 'position': position, 225 'color': (0, 1, 0), 226 'opacity': 0.3, 227 'draw_beauty': False, 228 'additive': True 229 }) 230 loc3 = ba.newnode('locator', 231 attrs={ 232 'shape': 'circleOutline', 233 'position': position, 234 'color': (0, 1, 0), 235 'opacity': 0.1, 236 'draw_beauty': False, 237 'additive': True 238 }) 239 self._nodes = [loc1, loc2, loc3] 240 ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]}) 241 ba.animate_array(loc2, 'size', 1, { 242 0.05: [0.0], 243 0.25: [self._r2 * 2.0] 244 }) 245 ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]}) 246 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.
251 def handlemessage(self, msg: Any) -> Any: 252 if isinstance(msg, ba.DieMessage): 253 for node in self._nodes: 254 node.delete() 255 self._nodes = [] 256 else: 257 super().handlemessage(msg)
General message handling; can be passed any message object.
259 def get_dist_from_point(self, pos: ba.Vec3) -> float: 260 """Given a point, returns distance squared from it.""" 261 return (pos - self._position).length()
Given a point, returns distance squared from it.
263 def do_hit_at_position(self, pos: Sequence[float], player: Player) -> bool: 264 """Handle a bomb hit at the given position.""" 265 # pylint: disable=too-many-statements 266 activity = self.activity 267 268 # Ignore hits if the game is over or if we've already been hit 269 if activity.has_ended() or self._hit or not self._nodes: 270 return False 271 272 diff = (ba.Vec3(pos) - self._position) 273 274 # Disregard Y difference. Our target point probably isn't exactly 275 # on the ground anyway. 276 diff[1] = 0.0 277 dist = diff.length() 278 279 bullseye = False 280 if dist <= self._r3 + self._rfudge: 281 # Inform our activity that we were hit 282 self._hit = True 283 activity.handlemessage(self.TargetHitMessage()) 284 keys: dict[float, Sequence[float]] = { 285 0.0: (1.0, 0.0, 0.0), 286 0.049: (1.0, 0.0, 0.0), 287 0.05: (1.0, 1.0, 1.0), 288 0.1: (0.0, 1.0, 0.0) 289 } 290 cdull = (0.3, 0.3, 0.3) 291 popupcolor: Sequence[float] 292 if dist <= self._r1 + self._rfudge: 293 bullseye = True 294 self._nodes[1].color = cdull 295 self._nodes[2].color = cdull 296 ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True) 297 popupscale = 1.8 298 popupcolor = (1, 1, 0, 1) 299 streak = player.streak 300 points = 10 + min(20, streak * 2) 301 ba.playsound(ba.getsound('bellHigh')) 302 if streak > 0: 303 ba.playsound( 304 ba.getsound( 305 'orchestraHit4' if streak > 3 else 306 'orchestraHit3' if streak > 2 else 307 'orchestraHit2' if streak > 1 else 'orchestraHit')) 308 elif dist <= self._r2 + self._rfudge: 309 self._nodes[0].color = cdull 310 self._nodes[2].color = cdull 311 ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True) 312 popupscale = 1.25 313 popupcolor = (1, 0.5, 0.2, 1) 314 points = 4 315 ba.playsound(ba.getsound('bellMed')) 316 else: 317 self._nodes[0].color = cdull 318 self._nodes[1].color = cdull 319 ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True) 320 popupscale = 1.0 321 popupcolor = (0.8, 0.3, 0.3, 1) 322 points = 2 323 ba.playsound(ba.getsound('bellLow')) 324 325 # Award points/etc.. (technically should probably leave this up 326 # to the activity). 327 popupstr = '+' + str(points) 328 329 # If there's more than 1 player in the game, include their 330 # names and colors so they know who got the hit. 331 if len(activity.players) > 1: 332 popupcolor = ba.safecolor(player.color, target_intensity=0.75) 333 popupstr += ' ' + player.getname() 334 PopupText(popupstr, 335 position=self._position, 336 color=popupcolor, 337 scale=popupscale).autoretain() 338 339 # Give this player's team points and update the score-board. 340 player.team.score += points 341 assert isinstance(activity, TargetPracticeGame) 342 activity.update_scoreboard() 343 344 # Also give this individual player points 345 # (only applies in teams mode). 346 assert activity.stats is not None 347 activity.stats.player_scored(player, 348 points, 349 showpoints=False, 350 screenmessage=False) 351 352 ba.animate_array(self._nodes[0], 'size', 1, { 353 0.8: self._nodes[0].size, 354 1.0: [0.0] 355 }) 356 ba.animate_array(self._nodes[1], 'size', 1, { 357 0.85: self._nodes[1].size, 358 1.05: [0.0] 359 }) 360 ba.animate_array(self._nodes[2], 'size', 1, { 361 0.9: self._nodes[2].size, 362 1.1: [0.0] 363 }) 364 ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage())) 365 366 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.