Source code for bascenev1lib.game.targetpractice

# Released under the MIT License. See LICENSE for details.
#
"""Implements Target Practice game."""

# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)

from __future__ import annotations

import random
from typing import TYPE_CHECKING, override

import bascenev1 as bs

from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.onscreencountdown import OnScreenCountdown
from bascenev1lib.actor.bomb import Bomb
from bascenev1lib.actor.popuptext import PopupText

if TYPE_CHECKING:
    from typing import Any, Sequence

    from bascenev1lib.actor.bomb import Blast


[docs] class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: self.streak = 0
[docs] class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: self.score = 0
# ba_meta export bascenev1.GameActivity
[docs] class TargetPracticeGame(bs.TeamGameActivity[Player, Team]): """Game where players try to hit targets with bombs.""" name = 'Target Practice' description = 'Bomb as many targets as you can.' available_settings = [ bs.IntSetting('Target Count', min_value=1, default=3), bs.BoolSetting('Enable Impact Bombs', default=True), bs.BoolSetting('Enable Triple Bombs', default=True), ] default_music = bs.MusicType.FORWARD_MARCH
[docs] @override @classmethod def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: return ['Doom Shroom']
[docs] @override @classmethod def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: # We support any teams or versus sessions. return issubclass(sessiontype, bs.CoopSession) or issubclass( sessiontype, bs.MultiTeamSession )
def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._targets: list[Target] = [] self._update_timer: bs.Timer | None = None self._countdown: OnScreenCountdown | None = None self._target_count = int(settings['Target Count']) self._enable_impact_bombs = bool(settings['Enable Impact Bombs']) self._enable_triple_bombs = bool(settings['Enable Triple Bombs'])
[docs] @override def on_team_join(self, team: Team) -> None: if self.has_begun(): self.update_scoreboard()
[docs] @override def on_begin(self) -> None: super().on_begin() self.update_scoreboard() # Number of targets is based on player count. for i in range(self._target_count): bs.timer(5.0 + i * 1.0, self._spawn_target) self._update_timer = bs.Timer(1.0, self._update, repeat=True) self._countdown = OnScreenCountdown(60, endcall=self.end_game) bs.timer(4.0, self._countdown.start)
[docs] @override def spawn_player(self, player: Player) -> bs.Actor: spawn_center = (0, 3, -5) pos = ( spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], spawn_center[2] + random.uniform(-1.5, 1.5), ) # Reset their streak. player.streak = 0 spaz = self.spawn_player_spaz(player, position=pos) # Give players permanent triple impact bombs and wire them up # to tell us when they drop a bomb. if self._enable_impact_bombs: spaz.bomb_type = 'impact' if self._enable_triple_bombs: spaz.set_bomb_count(3) spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb) return spaz
def _spawn_target(self) -> None: # Generate a few random points; we'll use whichever one is farthest # from our existing targets (don't want overlapping targets). points = [] for _i in range(4): # Calc a random point within a circle. while True: xpos = random.uniform(-1.0, 1.0) ypos = random.uniform(-1.0, 1.0) if xpos * xpos + ypos * ypos < 1.0: break points.append(bs.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos)) def get_min_dist_from_target(pnt: bs.Vec3) -> float: return min((t.get_dist_from_point(pnt) for t in self._targets)) # If we have existing targets, use the point with the highest # min-distance-from-targets. if self._targets: point = max(points, key=get_min_dist_from_target) else: point = points[0] self._targets.append(Target(position=point)) def _on_spaz_dropped_bomb(self, spaz: bs.Actor, bomb: bs.Actor) -> None: del spaz # Unused. # Wire up this bomb to inform us when it blows up. assert isinstance(bomb, Bomb) bomb.add_explode_callback(self._on_bomb_exploded) def _on_bomb_exploded(self, bomb: Bomb, blast: Blast) -> None: assert blast.node pos = blast.node.position # Debugging: throw a locator down where we landed. # bs.newnode('locator', attrs={'position':blast.node.position}) # Feed the explosion point to all our targets and get points in return. # Note: we operate on a copy of self._targets since the list may change # under us if we hit stuff (don't wanna get points for new targets). player = bomb.get_source_player(Player) if not player: # It's possible the player left after throwing the bomb. return bullseye = any( target.do_hit_at_position(pos, player) for target in list(self._targets) ) if bullseye: player.streak += 1 else: player.streak = 0 def _update(self) -> None: """Misc. periodic updating.""" # Clear out targets that have died. self._targets = [t for t in self._targets if t]
[docs] @override def handlemessage(self, msg: Any) -> Any: # When players die, respawn them. if isinstance(msg, bs.PlayerDiedMessage): super().handlemessage(msg) # Do standard stuff. player = msg.getplayer(Player) assert player is not None self.respawn_player(player) # Kick off a respawn. elif isinstance(msg, Target.TargetHitMessage): # A target is telling us it was hit and will die soon.. # ..so make another one. self._spawn_target() else: super().handlemessage(msg)
[docs] def update_scoreboard(self) -> None: """Update the game scoreboard with current team values.""" for team in self.teams: self._scoreboard.set_team_value(team, team.score)
[docs] @override def end_game(self) -> None: results = bs.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results)
[docs] class Target(bs.Actor): """A target practice target."""
[docs] class TargetHitMessage: """Inform an object a target was hit."""
def __init__(self, position: Sequence[float]): self._r1 = 0.45 self._r2 = 1.1 self._r3 = 2.0 self._rfudge = 0.15 super().__init__() self._position = bs.Vec3(position) self._hit = False # It can be handy to test with this on to make sure the projection # isn't too far off from the actual object. show_in_space = False loc1 = bs.newnode( 'locator', attrs={ 'shape': 'circle', 'position': position, 'color': (0, 1, 0), 'opacity': 0.5, 'draw_beauty': show_in_space, 'additive': True, }, ) loc2 = bs.newnode( 'locator', attrs={ 'shape': 'circleOutline', 'position': position, 'color': (0, 1, 0), 'opacity': 0.3, 'draw_beauty': False, 'additive': True, }, ) loc3 = bs.newnode( 'locator', attrs={ 'shape': 'circleOutline', 'position': position, 'color': (0, 1, 0), 'opacity': 0.1, 'draw_beauty': False, 'additive': True, }, ) self._nodes = [loc1, loc2, loc3] bs.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]}) bs.animate_array(loc2, 'size', 1, {0.05: [0.0], 0.25: [self._r2 * 2.0]}) bs.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]}) bs.getsound('laserReverse').play()
[docs] @override def exists(self) -> bool: return bool(self._nodes)
[docs] @override def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.DieMessage): for node in self._nodes: node.delete() self._nodes = [] else: super().handlemessage(msg)
[docs] def get_dist_from_point(self, pos: bs.Vec3) -> float: """Given a point, returns distance squared from it.""" return (pos - self._position).length()
[docs] def do_hit_at_position(self, pos: Sequence[float], player: Player) -> bool: """Handle a bomb hit at the given position.""" # pylint: disable=too-many-statements activity = self.activity # Ignore hits if the game is over or if we've already been hit if activity.has_ended() or self._hit or not self._nodes: return False diff = bs.Vec3(pos) - self._position # Disregard Y difference. Our target point probably isn't exactly # on the ground anyway. diff[1] = 0.0 dist = diff.length() bullseye = False if dist <= self._r3 + self._rfudge: # Inform our activity that we were hit self._hit = True activity.handlemessage(self.TargetHitMessage()) keys: dict[float, Sequence[float]] = { 0.0: (1.0, 0.0, 0.0), 0.049: (1.0, 0.0, 0.0), 0.05: (1.0, 1.0, 1.0), 0.1: (0.0, 1.0, 0.0), } cdull = (0.3, 0.3, 0.3) popupcolor: Sequence[float] if dist <= self._r1 + self._rfudge: bullseye = True self._nodes[1].color = cdull self._nodes[2].color = cdull bs.animate_array(self._nodes[0], 'color', 3, keys, loop=True) popupscale = 1.8 popupcolor = (1, 1, 0, 1) streak = player.streak points = 10 + min(20, streak * 2) bs.getsound('bellHigh').play() if streak > 0: bs.getsound( 'orchestraHit4' if streak > 3 else ( 'orchestraHit3' if streak > 2 else ( 'orchestraHit2' if streak > 1 else 'orchestraHit' ) ) ).play() elif dist <= self._r2 + self._rfudge: self._nodes[0].color = cdull self._nodes[2].color = cdull bs.animate_array(self._nodes[1], 'color', 3, keys, loop=True) popupscale = 1.25 popupcolor = (1, 0.5, 0.2, 1) points = 4 bs.getsound('bellMed').play() else: self._nodes[0].color = cdull self._nodes[1].color = cdull bs.animate_array(self._nodes[2], 'color', 3, keys, loop=True) popupscale = 1.0 popupcolor = (0.8, 0.3, 0.3, 1) points = 2 bs.getsound('bellLow').play() # Award points/etc.. (technically should probably leave this up # to the activity). popupstr = '+' + str(points) # If there's more than 1 player in the game, include their # names and colors so they know who got the hit. if len(activity.players) > 1: popupcolor = bs.safecolor(player.color, target_intensity=0.75) popupstr += ' ' + player.getname() PopupText( popupstr, position=self._position, color=popupcolor, scale=popupscale, ).autoretain() # Give this player's team points and update the score-board. player.team.score += points assert isinstance(activity, TargetPracticeGame) activity.update_scoreboard() # Also give this individual player points # (only applies in teams mode). assert activity.stats is not None activity.stats.player_scored( player, points, showpoints=False, screenmessage=False ) bs.animate_array( self._nodes[0], 'size', 1, {0.8: self._nodes[0].size, 1.0: [0.0]}, ) bs.animate_array( self._nodes[1], 'size', 1, {0.85: self._nodes[1].size, 1.05: [0.0]}, ) bs.animate_array( self._nodes[2], 'size', 1, {0.9: self._nodes[2].size, 1.1: [0.0]}, ) bs.timer(1.1, bs.Call(self.handlemessage, bs.DieMessage())) return bullseye