diff --git a/plugins/minigames/botsvsbots.py b/plugins/minigames/botsvsbots.py new file mode 100644 index 0000000..ffa903c --- /dev/null +++ b/plugins/minigames/botsvsbots.py @@ -0,0 +1,375 @@ +""" +Bots Fighting game. Pretty cool for if u want to bet with your mates, or let fate (randomess) decide on the winning team. + +-credits- +made by rabbitboom, aka Johnnyä»” (YouTube) or iCommerade (gaming world(???)) +original idea by Froshlee08 (hence a bot named in his honour) +shoutout to EraOS who gave me quite a lot of solution and Brotherboard who also helped. +""" + +# ba_meta require api 9 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING, override + + +import logging, random, weakref +import bascenev1 as bs +import efro.debug as Edebug + +from bascenev1lib.actor.spaz import Spaz, PunchHitMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.spazbot import ( + SpazBotDiedMessage, + SpazBotSet, + ChargerBot, + StickyBot, + SpazBot, + BrawlerBot, + TriggerBot, + BrawlerBotProShielded, + ChargerBotProShielded, + BouncyBot, + TriggerBotProShielded, + BomberBotProShielded, +) + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List + +class Player(bs.Player['Team']): + """Our player type for this game.""" + +class SpazBot2(SpazBot): + #custom bots that have custom behaviours requiring more msg and modules from spaz.py will have them imported here. + from bascenev1lib.actor.spaz import Spaz, PunchHitMessage + +class BouncyBotSemiLite(BouncyBot): + highlight = (1,1,0.8) + punchiness = 0.85 + run = False + default_boxing_gloves = False + charge_dist_min = 5 + points_mult = 1 + + +class FroshBot(SpazBot): + #Slightly beefier version of BomberBots, with slightly altered behaviour. + color = (0.13,0.13,0.13) + highlight = (0.2,1,1) + character = 'Bernard' + run = True + default_bomb_count = 2 + throw_rate = 0.8 + throwiness = 0.2 + punchiness = 0.85 + charge_dist_max = 3.0 + charge_speed_min = 0.3 + default_hitpoints = 1300 + +class FroshBotShielded(FroshBot): + default_shields = True + +class StickyBotShielded(StickyBot): + default_shields = True + +class IcePuncherBot(SpazBot2): + #A bot who can freeze anyone on punch and is immune to freezing. + color = (0,0,1) + highlight = (.2,.2,1) + character = 'Pascal' + run = True + punchiness = 0.8 + charge_dist_min = 6 + charge_dist_max = 9999 + charge_speed_min = 0.3 + charge_speed_max = 0.85 + throw_dist_min = 9999 + throw_dist_max = 9999 + + def handlemessage(self, msg): + #Add the ability to freeze anyone on punch. + if isinstance(msg, self.PunchHitMessage): + node = bs.getcollision().opposingnode + try: + node.handlemessage(bs.FreezeMessage()) + bs.getsound('freeze').play() + except Exception: + print('freeze failed.') + return super().handlemessage(msg) + elif isinstance(msg, bs.FreezeMessage): + pass #resistant to freezing. + else: + return super().handlemessage(msg) + +class IcePuncherBotShielded(IcePuncherBot): + default_shields = True + + +class GliderBot(BrawlerBot): + #A bot who can glide on terrain and navigate it safely as if it's ice skating. + color = (0.5,0.5,0.5) + highlight = (0,10,0) + character = 'B-9000' + charge_speed_min = 0.3 + charge_speed_max = 1.0 + + def __init__(self) -> None: + super().__init__() + self.node.hockey = True #Added the ability to move fast "like in hockey". + +class GliderBotShielded(GliderBot): + #Shielded version of GliderBot. + default_shields = True + + + +class TeamBotSet(SpazBotSet): + """Bots that can gather in a team and kill other bots.""" + + activity: BotsVSBotsGame + + def __init__ (self, team: Team) -> None: + super().__init__() + activity = bs.getactivity() + self._pro_bots = activity._pro_bots + self._bonus_bots = activity._bonus_bots + self._punching_bots_only = activity._punching_bots_only + self.spawn_time = activity.spawn_time + self._team = weakref.ref(team) + + + def colorforteam(self, spaz) -> None: + spaz.node.color = self.team.color + + + def yell(self) -> None: + for botlist in self._bot_lists: + for bot in botlist: + if bot: + assert bot.node + self.celebrate(self.spawn_time) + bs.Call(bot.node.handlemessage, 'jump_sound') + bs.timer(0, bs.Call(bot.node.handlemessage, 'attack_sound')) + + def _update(self) -> None: + # Update one of our bot lists each time through. + # First off, remove no-longer-existing bots from the list. + try: + bot_list = self._bot_lists[self._bot_update_list] = [ + b for b in self._bot_lists[self._bot_update_list] if b + ] + except Exception: + bot_list = [] + logging.exception( + 'Error updating bot list: %s', + self._bot_lists[self._bot_update_list], + ) + self._bot_update_list = ( + self._bot_update_list + 1 + ) % self._bot_list_count + + # Update our list of player points for the bots to use. + player_pts = [] + try: + for n in bs.getnodes(): + if n.getnodetype() == 'spaz': + s = n.getdelegate(object) + if isinstance(s, SpazBot): + if not s in self.get_living_bots(): + if s.is_alive(): + player_pts.append(( + bs.Vec3(n.position), + bs.Vec3(n.velocity))) + except Exception: + logging.exception('Error on bot-set _update.') + + for bot in bot_list: + bot.set_player_points(player_pts) + bot.update_ai() + + + + def get_bot_type(self): + bot_types = [ + BrawlerBot, + ChargerBot, + BouncyBotSemiLite, + ] + if self._punching_bots_only == False: + bot_types += [ + SpazBot, + StickyBot, + ] + if self._pro_bots == True: + bot_types += [ + BrawlerBotProShielded, + ChargerBotProShielded, + BouncyBot, + ] + if self._punching_bots_only == False: + bot_types += [ + BomberBotProShielded, + ] + if self._bonus_bots == True: + bot_types += [ + IcePuncherBot, + GliderBot, + ] + if self._punching_bots_only == False: + bot_types += [ + FroshBot, + ] + if self._bonus_bots and self._pro_bots == True: + bot_types += [ + IcePuncherBotShielded, + GliderBotShielded, + ] + if self._punching_bots_only == False: + bot_types += [ + FroshBotShielded, + StickyBotShielded, + ] + return random.choice(bot_types) + + + + + @property + def team(self) -> Team: + """The bot's team.""" + return self._team() + + + +class Team(bs.Team[TeamBotSet]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + self.bots = TeamBotSet(self) + + + + + +# ba_meta export bascenev1.GameActivity +class BotsVSBotsGame(bs.TeamGameActivity[bs.Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Bots VS Bots' + description = 'Sit down and enjoy mobs from your team fight the opposing team for you. \n Feel free to enjoy some popcorn with it or place a bet with your friends.' #, self._score_to_win + + @override + @classmethod + def get_available_settings( + cls, sessiontype: type[bs.Session] + ) -> list[bs.Setting]: + settings = [ + bs.IntSetting( + 'Bots Per Team', + min_value=5, + default=20, + increment=5, + ), + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Punchers only', default=False), + bs.BoolSetting('Pro bots', default=False), + bs.BoolSetting('Bonus bots', default=True), + ] + return settings + + @override + @classmethod + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @override + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._bots_per_team = int(settings['Bots Per Team']) + self._dingsound = bs.getsound('dingSmall') + self._punching_bots_only = bool(settings['Punchers only']) + self._pro_bots = bool(settings['Pro bots']) + self._bonus_bots = bool(settings['Bonus bots']) + self._epic_mode = bool(settings['Epic Mode']) + self._cheersound = bs.getsound('cheer') + self._marchsoundA = bs.getsound('footimpact01') + self._marchsoundB = bs.getsound('footimpact02') + self._marchsoundC = bs.getsound('footimpact03') + + # Base class overrides. + self.slow_motion = self._epic_mode + self.spawn_time = 1.6 if self._epic_mode else 4.0 + self.default_music = ( + bs.MusicType.TO_THE_DEATH + ) + + @override + def get_instance_description(self) -> str | Sequence: + return 'Enjoy the battle.' + @override + def get_instance_description_short(self) -> str | Sequence: + return 'Enjoy the battle.' + @override + def on_team_join(self, team: Team) -> None: + self.spawn_team(team) + + @override + def on_begin(self) -> None: + bs.TeamGameActivity.on_begin(self) + + def spawn_player(self, player: Player) -> None: + return None + + #def team_pois_set + + def spawn_team(self, team: Team) -> None: + camp = self.map.get_flag_position(team.id) + for i in range(self._bots_per_team): + team.bots.spawn_bot(team.bots.get_bot_type(), + (camp[0], camp[1] + 1.8, random.randrange(-4,5)), + self.spawn_time, team.bots.colorforteam) + bs.timer(float(self.spawn_time) + 1.7, team.bots.yell) + + + def update(self) -> None: + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0, self.end_game) + + def _get_living_teams(self) -> list[Team]: + return [ + team + for team in self.teams + if team.bots.have_living_bots() + ] + + + @override + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, SpazBotDiedMessage): + bs.pushcall(self.update) + + else: + return super().handlemessage(msg) + return None + + + + @override + def end_game(self) -> None: + if self.has_ended(): + return + results = bs.GameResults() + self._cheersound.play() + for team in self.teams: + team.bots.final_celebrate() + results.set_team_score(team, len(team.bots.get_living_bots())) + self.end(results=results)