diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b242572 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "githubPullRequests.ignoredPullRequestBranches": [ + "main" + ] +} \ No newline at end of file diff --git a/plugins/minigames.json b/plugins/minigames.json index 44e1e93..52ea068 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1079,6 +1079,76 @@ "versions": { "1.0.0": null } + }, + "ba_dark_fileds": { + "description": "Get to the other side and watch your step", + "external_url": "", + "authors": [ + { + "name": "Froshlee24", + "email": "", + "discord": "froshlee24" + } + ], + "versions": { + "1.0.0": null + } + }, + "onslaught_football": { + "description": "Onslaught but in football map", + "external_url": "", + "authors": [ + { + "name": "", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } + }, + "lame_fight": { + "description": "Save World With Super Powers", + "external_url": "", + "authors": [ + { + "name": "", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } + }, + "infinite_ninjas": { + "description": "How long can you survive from Ninjas??", + "external_url": "", + "authors": [ + { + "name": "", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } + }, + "gravity_falls": { + "description": "Trip to the moon", + "external_url": "", + "authors": [ + { + "name": "", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } } } } \ No newline at end of file diff --git a/plugins/minigames/ba_dark_fields.py b/plugins/minigames/ba_dark_fields.py new file mode 100644 index 0000000..2a82005 --- /dev/null +++ b/plugins/minigames/ba_dark_fields.py @@ -0,0 +1,291 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +"""Dark fields mini-game.""" + +# Minigame by Froshlee24 +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations +import random +from typing import TYPE_CHECKING + +import _babase +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor import bomb +from bascenev1._music import setmusic +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1._gameutils import animate_array +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.playerspaz import PlayerSpaz + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List, Dict, Type, Type + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + +# ba_meta export bascenev1.GameActivity +class DarkFieldsGame(bs.TeamGameActivity[Player, Team]): + + name = 'Dark Fields' + description = 'Get to the other side.' + available_settings = [ + bs.IntSetting('Score to Win', + min_value=1, + default=3, + ), + bs.IntChoiceSetting('Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting('Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Players as center of interest', default=True), + ] + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('football') + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + self._epic_mode = bool(settings['Epic Mode']) + self._center_of_interest = bool(settings['Players as center of interest']) + self._score_to_win_per_player = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + self._scoreboard = Scoreboard() + + shared = SharedObjects.get() + + self._scoreRegionMaterial = bs.Material() + self._scoreRegionMaterial.add_actions( + conditions=("they_have_material",shared.player_material), + actions=(("modify_part_collision","collide",True), + ("modify_part_collision","physical",False), + ("call","at_connect", self._onPlayerScores))) + + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else None) + + + def on_transition_in(self) -> None: + super().on_transition_in() + gnode = bs.getactivity().globalsnode + gnode.tint = (0.5,0.5,0.5) + + a = bs.newnode('locator',attrs={'shape':'box','position':(12.2,0,.1087926362), + 'color':(5,0,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[2.5,0.1,12.8]}) + + b = bs.newnode('locator',attrs={'shape':'box','position':(-12.1,0,.1087926362), + 'color':(0,0,5),'opacity':1,'draw_beauty':True,'additive':False,'size':[2.5,0.1,12.8]}) + + def on_begin(self) -> None: + # self._has_begun = False + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + self._score_to_win = (self._score_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + self.isUpdatingMines = False + self._scoreSound = bs.getsound('dingSmall') + + for p in self.players: + if p.actor is not None: + try: + p.actor.disconnect_controls_from_player() + except Exception: + print('Can\'t connect to player') + + self._scoreRegions = [] + defs = bs.getactivity().map.defs + self._scoreRegions.append(bs.NodeActor(bs.newnode('region', + attrs={'position':defs.boxes['goal1'][0:3], + 'scale':defs.boxes['goal1'][6:9], + 'type': 'box', + 'materials':(self._scoreRegionMaterial,)}))) + self.mines = [] + self.spawnMines() + bs.timer(0.8 if self.slow_motion else 1.7,self.start) + + def start(self): + # self._has_begun = True + self._show_info() + bs.timer(random.randrange(3,7),self.doRandomLighting) + if not self._epic_mode: + setmusic(bs.MusicType.SCARY) + animate_array(bs.getactivity().globalsnode,'tint',3,{0:(0.5,0.5,0.5),2:(0.2,0.2,0.2)}) + + for p in self.players: + self.doPlayer(p) + + def spawn_player(self, player): + if not self.has_begun(): + return + else: + self.doPlayer(player) + + def doPlayer(self,player): + pos = (-12.4,1,random.randrange(-5,5)) + player = self.spawn_player_spaz(player,pos) + player.connect_controls_to_player(enable_punch=False,enable_bomb=False) + player.node.is_area_of_interest = self._center_of_interest + + def _show_info(self) -> None: + if self.has_begun(): + super()._show_info() + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, self._score_to_win) + + def doRandomLighting(self): + bs.timer(random.randrange(3,7),self.doRandomLighting) + if self.isUpdatingMines: return + + delay = 0 + for mine in self.mines: + if mine.node.exists(): + pos = mine.node.position + bs.timer(delay,babase.Call(self.do_light,pos)) + delay += 0.005 if self._epic_mode else 0.01 + + def do_light(self,pos): + light = bs.newnode('light',attrs={ + 'position': pos, + 'volume_intensity_scale': 1.0, + 'radius':0.1, + 'color': (1,0,0) + }) + bs.animate(light, 'intensity', { 0: 2.0, 3.0: 0.0}) + bs.timer(3.0, light.delete) + + def spawnMines(self): + delay = 0 + h_range = [10,8,6,4,2,0,-2,-4,-6,-8,-10] + for h in h_range: + for i in range(random.randint(3,4)): + x = h+random.random() + y = random.randrange(-5,6)+(random.random()) + pos = (x,1,y) + bs.timer(delay,babase.Call(self.doMine,pos)) + delay += 0.015 if self._epic_mode else 0.04 + bs.timer(5.0,self.stopUpdateMines) + + def stopUpdateMines(self): + self.isUpdatingMines = False + + def updateMines(self): + if self.isUpdatingMines: return + self.isUpdatingMines = True + for m in self.mines: + m.node.delete() + self.mines = [] + self.spawnMines() + + + def doMine(self,pos): + b = bomb.Bomb(position=pos,bomb_type='land_mine').autoretain() + b.add_explode_callback(self._on_bomb_exploded) + b.arm() + self.mines.append(b) + + def _on_bomb_exploded(self, bomb: Bomb, blast: Blast) -> None: + assert blast.node + p = blast.node.position + pos = (p[0],p[1]+1,p[2]) + bs.timer(0.5,babase.Call(self.doMine,pos)) + + def _onPlayerScores(self): + player: Optional[Player] + try: + spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True) + except bs.NotFoundError: + return + + if not spaz.is_alive(): + return + + try: + player = spaz.getplayer(Player, True) + except bs.NotFoundError: + return + + if player.exists() and player.is_alive(): + player.team.score += 1 + self._scoreSound.play() + pos = player.actor.node.position + + animate_array(bs.getactivity().globalsnode,'tint',3,{0:(0.5,0.5,0.5),2.8:(0.2,0.2,0.2)}) + self._update_scoreboard() + + light = bs.newnode('light', + attrs={ + 'position': pos, + 'radius': 0.5, + 'color': (1, 0, 0) + }) + bs.animate(light, 'intensity', {0.0: 0, 0.1: 1, 0.5: 0}, loop=False) + bs.timer(1.0, light.delete) + + player.actor.handlemessage(bs.DieMessage( how=bs.DeathType.REACHED_GOAL)) + self.updateMines() + + if any(team.score >= self._score_to_win for team in self.teams): + bs.timer(0.5, self.end_game) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + else: + return super().handlemessage(msg) + return None + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) \ No newline at end of file diff --git a/plugins/minigames/gravity_falls.py b/plugins/minigames/gravity_falls.py new file mode 100644 index 0000000..f24b591 --- /dev/null +++ b/plugins/minigames/gravity_falls.py @@ -0,0 +1,34 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +## Made by MattZ45986 on GitHub +## Ported by: Freaku / @[Just] Freak#4999 + + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.game.elimination import EliminationGame + + + +# ba_meta require api 8 +# ba_meta export bascenev1.GameActivity +class GFGame(EliminationGame): + name = 'Gravity Falls' + + def spawn_player(self, player): + actor = self.spawn_player_spaz(player, (0,5,0)) + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + bs.timer(1,babase.Call(self.raise_player, player)) + return actor + + def raise_player(self, player): + if player.is_alive(): + try: + player.actor.node.handlemessage("impulse",player.actor.node.position[0],player.actor.node.position[1]+.5,player.actor.node.position[2],0,5,0, 3,10,0,0, 0,5,0) + except: pass + bs.timer(0.05,babase.Call(self.raise_player,player)) \ No newline at end of file diff --git a/plugins/minigames/infinite_ninjas.py b/plugins/minigames/infinite_ninjas.py new file mode 100644 index 0000000..c43cb65 --- /dev/null +++ b/plugins/minigames/infinite_ninjas.py @@ -0,0 +1,134 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 + +#Copy pasted from ExplodoRun by Blitz,just edited Bots and map 😝 + + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazbot import SpazBotSet, ChargerBot, SpazBotDiedMessage +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Type, Dict, List, Optional + +## MoreMinigames.py support ## +def ba_get_api_version(): + return 6 + +def ba_get_levels(): + return [babase._level.Level( + 'Infinite Ninjas',gametype=InfiniteNinjasGame, + settings={}, + preview_texture_name = 'footballStadiumPreview'), + babase._level.Level( + 'Epic Infinite Ninjas',gametype=InfiniteNinjasGame, + settings={'Epic Mode':True}, + preview_texture_name = 'footballStadiumPreview')] +## MoreMinigames.py support ## + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + +# ba_meta export bascenev1.GameActivity +class InfiniteNinjasGame(bs.TeamGameActivity[Player, Team]): + name = "Infinite Ninjas" + description = "How long can you survive from Ninjas??" + available_settings = [bs.BoolSetting('Epic Mode', default=False)] + scoreconfig = bs.ScoreConfig(label='Time', + scoretype=bs.ScoreType.MILLISECONDS, + lower_is_better=False) + default_music = bs.MusicType.TO_THE_DEATH + + def __init__(self, settings:dict): + settings['map'] = "Football Stadium" + self._epic_mode = settings.get('Epic Mode', False) + if self._epic_mode: + self.slow_motion = True + super().__init__(settings) + self._timer: Optional[OnScreenTimer] = None + self._winsound = bs.getsound('score') + self._won = False + self._bots = SpazBotSet() + self.wave = 1 + + def on_begin(self) -> None: + super().on_begin() + + self._timer = OnScreenTimer() + bs.timer(2.5, self._timer.start) + + #Bots Hehe + bs.timer(2.5,self.street) + + def street(self): + for a in range(self.wave): + p1 = random.choice([-5,-2.5,0,2.5,5]) + p3 = random.choice([-4.5,-4.14,-5,-3]) + time = random.choice([1,1.5,2.5,2]) + self._bots.spawn_bot(ChargerBot, pos=(p1,0.4,p3),spawn_time = time) + self.wave += 1 + + def botrespawn(self): + if not self._bots.have_living_bots(): + self.street() + def handlemessage(self, msg: Any) -> Any: + + # A player has died. + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard behavior. + self._won = True + self.end_game() + + # A spaz-bot has died. + elif isinstance(msg, SpazBotDiedMessage): + # Unfortunately the bot-set will always tell us there are living + # bots if we ask here (the currently-dying bot isn't officially + # marked dead yet) ..so lets push a call into the event loop to + # check once this guy has finished dying. + babase.pushcall(self.botrespawn) + + # Let the base class handle anything we don't. + else: + return super().handlemessage(msg) + return None + + # When this is called, we should fill out results and end the game + # *regardless* of whether is has been won. (this may be called due + # to a tournament ending or other external reason). + def end_game(self) -> None: + + # Stop our on-screen timer so players can see what they got. + assert self._timer is not None + self._timer.stop() + + results = bs.GameResults() + + # If we won, set our score to the elapsed time in milliseconds. + # (there should just be 1 team here since this is co-op). + # ..if we didn't win, leave scores as default (None) which means + # we lost. + if self._won: + elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0) + bs.cameraflash() + self._winsound.play() + for team in self.teams: + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage()) + results.set_team_score(team, elapsed_time_ms) + + # Ends the activity. + self.end(results) + + \ No newline at end of file diff --git a/plugins/minigames/lame_fight.py b/plugins/minigames/lame_fight.py new file mode 100644 index 0000000..e997a72 --- /dev/null +++ b/plugins/minigames/lame_fight.py @@ -0,0 +1,158 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.spazbot import SpazBotSet, ChargerBot, BrawlerBotProShielded, TriggerBotProShielded, ExplodeyBot, BomberBotProShielded, SpazBotDiedMessage +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Type, Dict, List, Optional + +def ba_get_api_version(): + return 6 + +def ba_get_levels(): + return [babase._level.Level( + 'Lame Fight', + gametype=LameFightGame, + settings={}, + preview_texture_name='courtyardPreview')] + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + +# ba_meta export bascenev1.GameActivity +class LameFightGame(bs.TeamGameActivity[Player, Team]): + name = "Lame Fight" + description = "Save World With Super Powers" + slow_motion = True + scoreconfig = bs.ScoreConfig(label='Time', + scoretype=bs.ScoreType.MILLISECONDS, + lower_is_better=True) + default_music = bs.MusicType.TO_THE_DEATH + + def __init__(self, settings:dict): + settings['map'] = "Courtyard" + super().__init__(settings) + self._timer: Optional[OnScreenTimer] = None + self._winsound = bs.getsound('score') + self._won = False + self._bots = SpazBotSet() + + def on_begin(self) -> None: + super().on_begin() + + self._timer = OnScreenTimer() + bs.timer(4.0, self._timer.start) + + #Bots Hehe + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(3,3,-2),spawn_time = 3.0)) + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(-3,3,-2),spawn_time = 3.0)) + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(5,3,-2),spawn_time = 3.0)) + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(-5,3,-2),spawn_time = 3.0)) + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(0,3,1),spawn_time = 3.0)) + bs.timer(1.0, lambda: self._bots.spawn_bot(ChargerBot, pos=(0,3,-5),spawn_time = 3.0)) + bs.timer(9.0, lambda: self._bots.spawn_bot(BomberBotProShielded, pos=(-7,5,-7.5),spawn_time = 3.0)) + bs.timer(9.0, lambda: self._bots.spawn_bot(BomberBotProShielded, pos=(7,5,-7.5),spawn_time = 3.0)) + bs.timer(9.0, lambda: self._bots.spawn_bot(BomberBotProShielded, pos=(7,5,1.5),spawn_time = 3.0)) + bs.timer(9.0, lambda: self._bots.spawn_bot(BomberBotProShielded, pos=(-7,5,1.5),spawn_time = 3.0)) + bs.timer(12.0, lambda: self._bots.spawn_bot(TriggerBotProShielded, pos=(-1,7,-8),spawn_time = 3.0)) + bs.timer(12.0, lambda: self._bots.spawn_bot(TriggerBotProShielded, pos=(1,7,-8),spawn_time = 3.0)) + bs.timer(15.0, lambda: self._bots.spawn_bot(ExplodeyBot, pos=(0,3,-5),spawn_time = 3.0)) + bs.timer(20.0, lambda: self._bots.spawn_bot(ExplodeyBot, pos=(0,3,1),spawn_time = 3.0)) + bs.timer(20.0, lambda: self._bots.spawn_bot(ExplodeyBot, pos=(-5,3,-2),spawn_time = 3.0)) + bs.timer(20.0, lambda: self._bots.spawn_bot(ExplodeyBot, pos=(5,3,-2),spawn_time = 3.0)) + bs.timer(30,self.street) + + def street(self): + bs.broadcastmessage("Lame Guys Are Here!",color = (1,0,0)) + for a in range(-1,2): + for b in range(-3,0): + self._bots.spawn_bot(BrawlerBotProShielded, pos=(a,3,b),spawn_time = 3.0) + + def spawn_player(self, player: Player) -> bs.Actor: + spawn_center = (0, 3, -2) + pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], + spawn_center[2] + random.uniform(-1.5, 1.5)) + spaz = self.spawn_player_spaz(player,position = pos) + p = ["Bigger Blast","Stronger Punch","Shield","Speed"] + Power = random.choice(p) + spaz.bomb_type = random.choice(["normal","sticky","ice","impact","normal","ice","sticky"]) + bs.broadcastmessage(f"Now You Have {Power}") + if Power == p[0]: + spaz.bomb_count = 3 + spaz.blast_radius = 2.5 + if Power == p[1]: + spaz._punch_cooldown = 350 + spaz._punch_power_scale = 2.0 + if Power == p[2]: + spaz.equip_shields() + if Power == p[3]: + spaz.node.hockey = True + return spaz + def _check_if_won(self) -> None: + if not self._bots.have_living_bots(): + self._won = True + self.end_game() + def handlemessage(self, msg: Any) -> Any: + + # A player has died. + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard behavior. + self.respawn_player(msg.getplayer(Player)) + + # A spaz-bot has died. + elif isinstance(msg, SpazBotDiedMessage): + # Unfortunately the bot-set will always tell us there are living + # bots if we ask here (the currently-dying bot isn't officially + # marked dead yet) ..so lets push a call into the event loop to + # check once this guy has finished dying. + babase.pushcall(self._check_if_won) + + # Let the base class handle anything we don't. + else: + return super().handlemessage(msg) + return None + + # When this is called, we should fill out results and end the game + # *regardless* of whether is has been won. (this may be called due + # to a tournament ending or other external reason). + def end_game(self) -> None: + + # Stop our on-screen timer so players can see what they got. + assert self._timer is not None + self._timer.stop() + + results = bs.GameResults() + + # If we won, set our score to the elapsed time in milliseconds. + # (there should just be 1 team here since this is co-op). + # ..if we didn't win, leave scores as default (None) which means + # we lost. + if self._won: + elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0) + bs.cameraflash() + self._winsound.play() + for team in self.teams: + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage()) + results.set_team_score(team, elapsed_time_ms) + + # Ends the activity. + self.end(results) + + \ No newline at end of file diff --git a/plugins/minigames/onslaught_football.py b/plugins/minigames/onslaught_football.py new file mode 100644 index 0000000..06a5c5c --- /dev/null +++ b/plugins/minigames/onslaught_football.py @@ -0,0 +1,1023 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations +from asyncio import base_subprocess + +import math +import random +from enum import Enum, unique +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.popuptext import PopupText +from bascenev1lib.actor.bomb import TNTSpawner +from bascenev1lib.actor.playerspaz import PlayerSpazHurtMessage +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.controlsguide import ControlsGuide +from bascenev1lib.actor.powerupbox import PowerupBox, PowerupBoxFactory +from bascenev1lib.actor.spazbot import ( + SpazBotDiedMessage, + SpazBotSet, + ChargerBot, + StickyBot, + BomberBot, + BomberBotLite, + BrawlerBot, + BrawlerBotLite, + TriggerBot, + BomberBotStaticLite, + TriggerBotStatic, + BomberBotProStatic, + TriggerBotPro, + ExplodeyBot, + BrawlerBotProShielded, + ChargerBotProShielded, + BomberBotPro, + TriggerBotProShielded, + BrawlerBotPro, + BomberBotProShielded, +) + +if TYPE_CHECKING: + from typing import Any, Sequence + from bascenev1lib.actor.spazbot import SpazBot + + +@dataclass +class Wave: + """A wave of enemies.""" + + entries: list[Spawn | Spacing | Delay | None] + base_angle: float = 0.0 + + +@dataclass +class Spawn: + """A bot spawn event in a wave.""" + + bottype: type[SpazBot] | str + point: Point | None = None + spacing: float = 5.0 + + +@dataclass +class Spacing: + """Empty space in a wave.""" + + spacing: float = 5.0 + + +@dataclass +class Delay: + """A delay between events in a wave.""" + + duration: float + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.has_been_hurt = False + self.respawn_wave = 0 + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +class OnslaughtFootballGame(bs.CoopGameActivity[Player, Team]): + """Co-op game where players try to survive attacking waves of enemies.""" + + name = 'Onslaught' + description = 'Defeat all enemies.' + + tips: list[str | babase.GameTip] = [ + 'Hold any button to run.' + ' (Trigger buttons work well if you have them)', + 'Try tricking enemies into killing eachother or running off cliffs.', + 'Try \'Cooking off\' bombs for a second or two before throwing them.', + 'It\'s easier to win with a friend or two helping.', + 'If you stay in one place, you\'re toast. Run and dodge to survive..', + 'Practice using your momentum to throw bombs more accurately.', + 'Your punches do much more damage if you are running or spinning.', + ] + + # Show messages when players die since it matters here. + announce_player_deaths = True + + def __init__(self, settings: dict): + super().__init__(settings) + self._new_wave_sound = bs.getsound('scoreHit01') + self._winsound = bs.getsound('score') + self._cashregistersound = bs.getsound('cashRegister') + self._a_player_has_been_hurt = False + self._player_has_dropped_bomb = False + self._spawn_center = (0, 0.2, 0) + self._tntspawnpos = (0, 0.95, -0.77) + self._powerup_center = (0, 1.5, 0) + self._powerup_spread = (6.0, 4.0) + self._scoreboard: Scoreboard | None = None + self._game_over = False + self._wavenum = 0 + self._can_end_wave = True + self._score = 0 + self._time_bonus = 0 + self._spawn_info_text: bs.NodeActor | None = None + self._dingsound = bs.getsound('dingSmall') + self._dingsoundhigh = bs.getsound('dingSmallHigh') + self._have_tnt = False + self._excluded_powerups: list[str] | None = None + self._waves: list[Wave] = [] + self._tntspawner: TNTSpawner | None = None + self._bots: SpazBotSet | None = None + self._powerup_drop_timer: bs.Timer | None = None + self._time_bonus_timer: bs.Timer | None = None + self._time_bonus_text: bs.NodeActor | None = None + self._flawless_bonus: int | None = None + self._wave_text: bs.NodeActor | None = None + self._wave_update_timer: bs.Timer | None = None + self._throw_off_kills = 0 + self._land_mine_kills = 0 + self._tnt_kills = 0 + + self._epic_mode = bool(settings['Epic Mode']) + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.ONSLAUGHT + ) + + def on_transition_in(self) -> None: + super().on_transition_in() + self._spawn_info_text = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'position': (15, -130), + 'h_attach': 'left', + 'v_attach': 'top', + 'scale': 0.55, + 'color': (0.3, 0.8, 0.3, 1.0), + 'text': '', + }, + ) + ) + self._scoreboard = Scoreboard( + label=babase.Lstr(resource='scoreText'), score_split=0.5 + ) + + def on_begin(self) -> None: + super().on_begin() + self._have_tnt = True + self._excluded_powerups = [] + self._waves = [] + bs.timer(4.0, self._start_powerup_drops) + + # Our TNT spawner (if applicable). + if self._have_tnt: + self._tntspawner = TNTSpawner(position=self._tntspawnpos) + + self.setup_low_life_warning_sound() + self._update_scores() + self._bots = SpazBotSet() + bs.timer(4.0, self._start_updating_waves) + self._next_ffa_start_index = random.randrange( + len(self.map.get_def_points('ffa_spawn')) + ) + + def _get_dist_grp_totals(self, grps: list[Any]) -> tuple[int, int]: + totalpts = 0 + totaldudes = 0 + for grp in grps: + for grpentry in grp: + dudes = grpentry[1] + totalpts += grpentry[0] * dudes + totaldudes += dudes + return totalpts, totaldudes + + def _get_distribution( + self, + target_points: int, + min_dudes: int, + max_dudes: int, + group_count: int, + max_level: int, + ) -> list[list[tuple[int, int]]]: + """Calculate a distribution of bad guys given some params.""" + max_iterations = 10 + max_dudes * 2 + + groups: list[list[tuple[int, int]]] = [] + for _g in range(group_count): + groups.append([]) + types = [1] + if max_level > 1: + types.append(2) + if max_level > 2: + types.append(3) + if max_level > 3: + types.append(4) + for iteration in range(max_iterations): + diff = self._add_dist_entry_if_possible( + groups, max_dudes, target_points, types + ) + + total_points, total_dudes = self._get_dist_grp_totals(groups) + full = total_points >= target_points + + if full: + # Every so often, delete a random entry just to + # shake up our distribution. + if random.random() < 0.2 and iteration != max_iterations - 1: + self._delete_random_dist_entry(groups) + + # If we don't have enough dudes, kill the group with + # the biggest point value. + elif ( + total_dudes < min_dudes and iteration != max_iterations - 1 + ): + self._delete_biggest_dist_entry(groups) + + # If we've got too many dudes, kill the group with the + # smallest point value. + elif ( + total_dudes > max_dudes and iteration != max_iterations - 1 + ): + self._delete_smallest_dist_entry(groups) + + # Close enough.. we're done. + else: + if diff == 0: + break + + return groups + + def _add_dist_entry_if_possible( + self, + groups: list[list[tuple[int, int]]], + max_dudes: int, + target_points: int, + types: list[int], + ) -> int: + # See how much we're off our target by. + total_points, total_dudes = self._get_dist_grp_totals(groups) + diff = target_points - total_points + dudes_diff = max_dudes - total_dudes + + # Add an entry if one will fit. + value = types[random.randrange(len(types))] + group = groups[random.randrange(len(groups))] + if not group: + max_count = random.randint(1, 6) + else: + max_count = 2 * random.randint(1, 3) + max_count = min(max_count, dudes_diff) + count = min(max_count, diff // value) + if count > 0: + group.append((value, count)) + total_points += value * count + total_dudes += count + diff = target_points - total_points + return diff + + def _delete_smallest_dist_entry( + self, groups: list[list[tuple[int, int]]] + ) -> None: + smallest_value = 9999 + smallest_entry = None + smallest_entry_group = None + for group in groups: + for entry in group: + if entry[0] < smallest_value or smallest_entry is None: + smallest_value = entry[0] + smallest_entry = entry + smallest_entry_group = group + assert smallest_entry is not None + assert smallest_entry_group is not None + smallest_entry_group.remove(smallest_entry) + + def _delete_biggest_dist_entry( + self, groups: list[list[tuple[int, int]]] + ) -> None: + biggest_value = 9999 + biggest_entry = None + biggest_entry_group = None + for group in groups: + for entry in group: + if entry[0] > biggest_value or biggest_entry is None: + biggest_value = entry[0] + biggest_entry = entry + biggest_entry_group = group + if biggest_entry is not None: + assert biggest_entry_group is not None + biggest_entry_group.remove(biggest_entry) + + def _delete_random_dist_entry( + self, groups: list[list[tuple[int, int]]] + ) -> None: + entry_count = 0 + for group in groups: + for _ in group: + entry_count += 1 + if entry_count > 1: + del_entry = random.randrange(entry_count) + entry_count = 0 + for group in groups: + for entry in group: + if entry_count == del_entry: + group.remove(entry) + break + entry_count += 1 + + def spawn_player(self, player: Player) -> bs.Actor: + + # We keep track of who got hurt each wave for score purposes. + player.has_been_hurt = False + pos = ( + self._spawn_center[0] + random.uniform(-1.5, 1.5), + self._spawn_center[1], + self._spawn_center[2] + random.uniform(-1.5, 1.5), + ) + spaz = self.spawn_player_spaz(player, position=pos) + spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) + return spaz + + def _handle_player_dropped_bomb( + self, player: bs.Actor, bomb: bs.Actor + ) -> None: + del player, bomb # Unused. + self._player_has_dropped_bomb = True + + def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: + poweruptype = PowerupBoxFactory.get().get_random_powerup_type( + forcetype=poweruptype, excludetypes=self._excluded_powerups + ) + PowerupBox( + position=self.map.powerup_spawn_points[index], + poweruptype=poweruptype, + ).autoretain() + + def _start_powerup_drops(self) -> None: + self._powerup_drop_timer = bs.Timer( + 3.0, bs.WeakCall(self._drop_powerups), repeat=True + ) + + def _drop_powerups( + self, standard_points: bool = False, poweruptype: str | None = None + ) -> None: + """Generic powerup drop.""" + if standard_points: + points = self.map.powerup_spawn_points + for i in range(len(points)): + bs.timer( + 1.0 + i * 0.5, + bs.WeakCall( + self._drop_powerup, i, poweruptype if i == 0 else None + ), + ) + else: + point = ( + self._powerup_center[0] + + random.uniform( + -1.0 * self._powerup_spread[0], + 1.0 * self._powerup_spread[0], + ), + self._powerup_center[1], + self._powerup_center[2] + + random.uniform( + -self._powerup_spread[1], self._powerup_spread[1] + ), + ) + + # Drop one random one somewhere. + PowerupBox( + position=point, + poweruptype=PowerupBoxFactory.get().get_random_powerup_type( + excludetypes=self._excluded_powerups + ), + ).autoretain() + + def do_end(self, outcome: str, delay: float = 0.0) -> None: + """End the game with the specified outcome.""" + if outcome == 'defeat': + self.fade_to_red() + score: int | None + if self._wavenum >= 2: + score = self._score + fail_message = None + else: + score = None + fail_message = babase.Lstr(resource='reachWave2Text') + self.end( + { + 'outcome': outcome, + 'score': score, + 'fail_message': fail_message, + 'playerinfos': self.initialplayerinfos, + }, + delay=delay, + ) + + def _update_waves(self) -> None: + + # If we have no living bots, go to the next wave. + assert self._bots is not None + if ( + self._can_end_wave + and not self._bots.have_living_bots() + and not self._game_over + ): + self._can_end_wave = False + self._time_bonus_timer = None + self._time_bonus_text = None + base_delay = 0.0 + + # Reward time bonus. + if self._time_bonus > 0: + bs.timer(0, babase.Call(self._cashregistersound.play)) + bs.timer( + base_delay, + bs.WeakCall(self._award_time_bonus, self._time_bonus), + ) + base_delay += 1.0 + + # Reward flawless bonus. + if self._wavenum > 0: + have_flawless = False + for player in self.players: + if player.is_alive() and not player.has_been_hurt: + have_flawless = True + bs.timer( + base_delay, + bs.WeakCall(self._award_flawless_bonus, player), + ) + player.has_been_hurt = False # reset + if have_flawless: + base_delay += 1.0 + + self._wavenum += 1 + + # Short celebration after waves. + if self._wavenum > 1: + self.celebrate(0.5) + bs.timer(base_delay, bs.WeakCall(self._start_next_wave)) + + def _award_completion_bonus(self) -> None: + self._cashregistersound.play() + for player in self.players: + try: + if player.is_alive(): + assert self.initialplayerinfos is not None + self.stats.player_scored( + player, + int(100 / len(self.initialplayerinfos)), + scale=1.4, + color=(0.6, 0.6, 1.0, 1.0), + title=babase.Lstr(resource='completionBonusText'), + screenmessage=False, + ) + except Exception: + babase.print_exception() + + def _award_time_bonus(self, bonus: int) -> None: + self._cashregistersound.play() + PopupText( + babase.Lstr( + value='+${A} ${B}', + subs=[ + ('${A}', str(bonus)), + ('${B}', babase.Lstr(resource='timeBonusText')), + ], + ), + color=(1, 1, 0.5, 1), + scale=1.0, + position=(0, 3, -1), + ).autoretain() + self._score += self._time_bonus + self._update_scores() + + def _award_flawless_bonus(self, player: Player) -> None: + self._cashregistersound.play() + try: + if player.is_alive(): + assert self._flawless_bonus is not None + self.stats.player_scored( + player, + self._flawless_bonus, + scale=1.2, + color=(0.6, 1.0, 0.6, 1.0), + title=babase.Lstr(resource='flawlessWaveText'), + screenmessage=False, + ) + except Exception: + babase.print_exception() + + def _start_time_bonus_timer(self) -> None: + self._time_bonus_timer = bs.Timer( + 1.0, bs.WeakCall(self._update_time_bonus), repeat=True + ) + + def _update_player_spawn_info(self) -> None: + + # If we have no living players lets just blank this. + assert self._spawn_info_text is not None + assert self._spawn_info_text.node + if not any(player.is_alive() for player in self.teams[0].players): + self._spawn_info_text.node.text = '' + else: + text: str | babase.Lstr = '' + for player in self.players: + if not player.is_alive(): + rtxt = babase.Lstr( + resource='onslaughtRespawnText', + subs=[ + ('${PLAYER}', player.getname()), + ('${WAVE}', str(player.respawn_wave)), + ], + ) + text = babase.Lstr( + value='${A}${B}\n', + subs=[ + ('${A}', text), + ('${B}', rtxt), + ], + ) + self._spawn_info_text.node.text = text + + def _respawn_players_for_wave(self) -> None: + # Respawn applicable players. + if self._wavenum > 1 and not self.is_waiting_for_continue(): + for player in self.players: + if ( + not player.is_alive() + and player.respawn_wave == self._wavenum + ): + self.spawn_player(player) + self._update_player_spawn_info() + + def _setup_wave_spawns(self, wave: Wave) -> None: + tval = 0.0 + dtime = 0.2 + if self._wavenum == 1: + spawn_time = 3.973 + tval += 0.5 + else: + spawn_time = 2.648 + + bot_angle = wave.base_angle + self._time_bonus = 0 + self._flawless_bonus = 0 + for info in wave.entries: + if info is None: + continue + if isinstance(info, Delay): + spawn_time += info.duration + continue + if isinstance(info, Spacing): + bot_angle += info.spacing + continue + bot_type_2 = info.bottype + if bot_type_2 is not None: + assert not isinstance(bot_type_2, str) + self._time_bonus += bot_type_2.points_mult * 20 + self._flawless_bonus += bot_type_2.points_mult * 5 + + if self.map.name == 'Doom Shroom': + tval += dtime + spacing = info.spacing + bot_angle += spacing * 0.5 + if bot_type_2 is not None: + tcall = bs.WeakCall( + self.add_bot_at_angle, bot_angle, bot_type_2, spawn_time + ) + bs.timer(tval, tcall) + tval += dtime + bot_angle += spacing * 0.5 + else: + assert bot_type_2 is not None + spcall = bs.WeakCall( + self.add_bot_at_point, bot_type_2, spawn_time + ) + bs.timer(tval, spcall) + + # We can end the wave after all the spawning happens. + bs.timer( + tval + spawn_time - dtime + 0.01, + bs.WeakCall(self._set_can_end_wave), + ) + + def _start_next_wave(self) -> None: + + # This can happen if we beat a wave as we die. + # We don't wanna respawn players and whatnot if this happens. + if self._game_over: + return + + self._respawn_players_for_wave() + wave = self._generate_random_wave() + self._setup_wave_spawns(wave) + self._update_wave_ui_and_bonuses() + bs.timer(0.4, babase.Call(self._new_wave_sound.play)) + + def _update_wave_ui_and_bonuses(self) -> None: + self.show_zoom_message( + babase.Lstr( + value='${A} ${B}', + subs=[ + ('${A}', babase.Lstr(resource='waveText')), + ('${B}', str(self._wavenum)), + ], + ), + scale=1.0, + duration=1.0, + trail=True, + ) + + # Reset our time bonus. + tbtcolor = (1, 1, 0, 1) + tbttxt = babase.Lstr( + value='${A}: ${B}', + subs=[ + ('${A}', babase.Lstr(resource='timeBonusText')), + ('${B}', str(self._time_bonus)), + ], + ) + self._time_bonus_text = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'vr_depth': -30, + 'color': tbtcolor, + 'shadow': 1.0, + 'flatness': 1.0, + 'position': (0, -60), + 'scale': 0.8, + 'text': tbttxt, + }, + ) + ) + + bs.timer(5.0, bs.WeakCall(self._start_time_bonus_timer)) + wtcolor = (1, 1, 1, 1) + wttxt = babase.Lstr( + value='${A} ${B}', + subs=[ + ('${A}', babase.Lstr(resource='waveText')), + ('${B}', str(self._wavenum) + ('')), + ], + ) + self._wave_text = bs.NodeActor( + bs.newnode( + 'text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'vr_depth': -10, + 'color': wtcolor, + 'shadow': 1.0, + 'flatness': 1.0, + 'position': (0, -40), + 'scale': 1.3, + 'text': wttxt, + }, + ) + ) + + def _bot_levels_for_wave(self) -> list[list[type[SpazBot]]]: + level = self._wavenum + bot_types = [ + BomberBot, + BrawlerBot, + TriggerBot, + ChargerBot, + BomberBotPro, + BrawlerBotPro, + TriggerBotPro, + BomberBotProShielded, + ExplodeyBot, + ChargerBotProShielded, + StickyBot, + BrawlerBotProShielded, + TriggerBotProShielded, + ] + if level > 5: + bot_types += [ + ExplodeyBot, + TriggerBotProShielded, + BrawlerBotProShielded, + ChargerBotProShielded, + ] + if level > 7: + bot_types += [ + ExplodeyBot, + TriggerBotProShielded, + BrawlerBotProShielded, + ChargerBotProShielded, + ] + if level > 10: + bot_types += [ + TriggerBotProShielded, + TriggerBotProShielded, + TriggerBotProShielded, + TriggerBotProShielded, + ] + if level > 13: + bot_types += [ + TriggerBotProShielded, + TriggerBotProShielded, + TriggerBotProShielded, + TriggerBotProShielded, + ] + bot_levels = [ + [b for b in bot_types if b.points_mult == 1], + [b for b in bot_types if b.points_mult == 2], + [b for b in bot_types if b.points_mult == 3], + [b for b in bot_types if b.points_mult == 4], + ] + + # Make sure all lists have something in them + if not all(bot_levels): + raise RuntimeError('Got empty bot level') + return bot_levels + + def _add_entries_for_distribution_group( + self, + group: list[tuple[int, int]], + bot_levels: list[list[type[SpazBot]]], + all_entries: list[Spawn | Spacing | Delay | None], + ) -> None: + entries: list[Spawn | Spacing | Delay | None] = [] + for entry in group: + bot_level = bot_levels[entry[0] - 1] + bot_type = bot_level[random.randrange(len(bot_level))] + rval = random.random() + if rval < 0.5: + spacing = 10.0 + elif rval < 0.9: + spacing = 20.0 + else: + spacing = 40.0 + split = random.random() > 0.3 + for i in range(entry[1]): + if split and i % 2 == 0: + entries.insert(0, Spawn(bot_type, spacing=spacing)) + else: + entries.append(Spawn(bot_type, spacing=spacing)) + if entries: + all_entries += entries + all_entries.append(Spacing(40.0 if random.random() < 0.5 else 80.0)) + + def _generate_random_wave(self) -> Wave: + level = self._wavenum + bot_levels = self._bot_levels_for_wave() + + target_points = level * 3 - 2 + min_dudes = min(1 + level // 3, 10) + max_dudes = min(10, level + 1) + max_level = ( + 4 if level > 6 else (3 if level > 3 else (2 if level > 2 else 1)) + ) + group_count = 3 + distribution = self._get_distribution( + target_points, min_dudes, max_dudes, group_count, max_level + ) + all_entries: list[Spawn | Spacing | Delay | None] = [] + for group in distribution: + self._add_entries_for_distribution_group( + group, bot_levels, all_entries + ) + angle_rand = random.random() + if angle_rand > 0.75: + base_angle = 130.0 + elif angle_rand > 0.5: + base_angle = 210.0 + elif angle_rand > 0.25: + base_angle = 20.0 + else: + base_angle = -30.0 + base_angle += (0.5 - random.random()) * 20.0 + wave = Wave(base_angle=base_angle, entries=all_entries) + return wave + + def add_bot_at_point( + self, spaz_type: type[SpazBot], spawn_time: float = 1.0 + ) -> None: + """Add a new bot at a specified named point.""" + if self._game_over: + return + def _getpt() -> Sequence[float]: + point = self.map.get_def_points( + 'ffa_spawn')[self._next_ffa_start_index] + self._next_ffa_start_index = ( + self._next_ffa_start_index + 1) % len( + self.map.get_def_points('ffa_spawn') + ) + x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3]) + z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5]) + point = ( + point[0] + random.uniform(*x_range), + point[1], + point[2] + random.uniform(*z_range), + ) + return point + pointpos = _getpt() + + assert self._bots is not None + self._bots.spawn_bot(spaz_type, pos=pointpos, spawn_time=spawn_time) + + def add_bot_at_angle( + self, angle: float, spaz_type: type[SpazBot], spawn_time: float = 1.0 + ) -> None: + """Add a new bot at a specified angle (for circular maps).""" + if self._game_over: + return + angle_radians = angle / 57.2957795 + xval = math.sin(angle_radians) * 1.06 + zval = math.cos(angle_radians) * 1.06 + point = (xval / 0.125, 2.3, (zval / 0.2) - 3.7) + assert self._bots is not None + self._bots.spawn_bot(spaz_type, pos=point, spawn_time=spawn_time) + + def _update_time_bonus(self) -> None: + self._time_bonus = int(self._time_bonus * 0.93) + if self._time_bonus > 0 and self._time_bonus_text is not None: + assert self._time_bonus_text.node + self._time_bonus_text.node.text = babase.Lstr( + value='${A}: ${B}', + subs=[ + ('${A}', babase.Lstr(resource='timeBonusText')), + ('${B}', str(self._time_bonus)), + ], + ) + else: + self._time_bonus_text = None + + def _start_updating_waves(self) -> None: + self._wave_update_timer = bs.Timer( + 2.0, bs.WeakCall(self._update_waves), repeat=True + ) + + def _update_scores(self) -> None: + score = self._score + assert self._scoreboard is not None + self._scoreboard.set_team_value(self.teams[0], score, max_score=None) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, PlayerSpazHurtMessage): + msg.spaz.getplayer(Player, True).has_been_hurt = True + self._a_player_has_been_hurt = True + + elif isinstance(msg, bs.PlayerScoredMessage): + self._score += msg.score + self._update_scores() + + elif isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard behavior. + player = msg.getplayer(Player) + self._a_player_has_been_hurt = True + + # Make note with the player when they can respawn: + if self._wavenum < 10: + player.respawn_wave = max(2, self._wavenum + 1) + elif self._wavenum < 15: + player.respawn_wave = max(2, self._wavenum + 2) + else: + player.respawn_wave = max(2, self._wavenum + 3) + bs.timer(0.1, self._update_player_spawn_info) + bs.timer(0.1, self._checkroundover) + + elif isinstance(msg, SpazBotDiedMessage): + pts, importance = msg.spazbot.get_death_points(msg.how) + if msg.killerplayer is not None: + target: Sequence[float] | None + if msg.spazbot.node: + target = msg.spazbot.node.position + else: + target = None + + killerplayer = msg.killerplayer + self.stats.player_scored( + killerplayer, + pts, + target=target, + kill=True, + screenmessage=False, + importance=importance, + ) + self._dingsound.play(volume=0.6) if importance == 1 else self._dingsoundhigh.play(volume=0.6) + + # Normally we pull scores from the score-set, but if there's + # no player lets be explicit. + else: + self._score += pts + self._update_scores() + else: + super().handlemessage(msg) + + def _handle_uber_kill_achievements(self, msg: SpazBotDiedMessage) -> None: + + # Uber mine achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): + self._land_mine_kills += 1 + if self._land_mine_kills >= 6: + self._award_achievement('Gold Miner') + + # Uber tnt achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): + self._tnt_kills += 1 + if self._tnt_kills >= 6: + bs.timer( + 0.5, bs.WeakCall(self._award_achievement, 'TNT Terror') + ) + + def _handle_pro_kill_achievements(self, msg: SpazBotDiedMessage) -> None: + + # TNT achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): + self._tnt_kills += 1 + if self._tnt_kills >= 3: + bs.timer( + 0.5, + bs.WeakCall( + self._award_achievement, 'Boom Goes the Dynamite' + ), + ) + + def _handle_rookie_kill_achievements(self, msg: SpazBotDiedMessage) -> None: + # Land-mine achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): + self._land_mine_kills += 1 + if self._land_mine_kills >= 3: + self._award_achievement('Mine Games') + + def _handle_training_kill_achievements( + self, msg: SpazBotDiedMessage + ) -> None: + # Toss-off-map achievement: + if msg.spazbot.last_attacked_type == ('picked_up', 'default'): + self._throw_off_kills += 1 + if self._throw_off_kills >= 3: + self._award_achievement('Off You Go Then') + + def _set_can_end_wave(self) -> None: + self._can_end_wave = True + + def end_game(self) -> None: + # Tell our bots to celebrate just to rub it in. + assert self._bots is not None + self._bots.final_celebrate() + self._game_over = True + self.do_end('defeat', delay=2.0) + bs.setmusic(None) + + def on_continue(self) -> None: + for player in self.players: + if not player.is_alive(): + self.spawn_player(player) + + def _checkroundover(self) -> None: + """Potentially end the round based on the state of the game.""" + if self.has_ended(): + return + if not any(player.is_alive() for player in self.teams[0].players): + # Allow continuing after wave 1. + if self._wavenum > 1: + self.continue_or_end_game() + else: + self.end_game() + +# ba_meta export plugin +class CustomOnslaughtLevel(babase.Plugin): + def on_app_running(self) -> None: + babase.app.classic.add_coop_practice_level( + bs._level.Level( + 'Onslaught Football', + gametype=OnslaughtFootballGame, + settings={ + 'map': 'Football Stadium', + 'Epic Mode': False, + }, + preview_texture_name='footballStadiumPreview', + ) + ) + babase.app.classic.add_coop_practice_level( + bs._level.Level( + 'Onslaught Football Epic', + gametype=OnslaughtFootballGame, + settings={ + 'map': 'Football Stadium', + 'Epic Mode': True, + }, + preview_texture_name='footballStadiumPreview', + ) + )