bombsquad-plugin-manager/plugins/minigames/botsvsbots.py

372 lines
11 KiB
Python
Raw Normal View History

2025-10-28 16:42:56 +00:00
"""
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
2025-10-28 16:44:49 +00:00
import logging
import random
import weakref
2025-10-28 16:42:56 +00:00
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
2025-10-28 16:44:49 +00:00
2025-10-28 16:42:56 +00:00
class Player(bs.Player['Team']):
"""Our player type for this game."""
2025-10-28 16:44:49 +00:00
2025-10-28 16:42:56 +00:00
class SpazBot2(SpazBot):
2025-10-28 16:44:49 +00:00
# custom bots that have custom behaviours requiring more msg and modules from spaz.py will have them imported here.
2025-10-28 16:42:56 +00:00
from bascenev1lib.actor.spaz import Spaz, PunchHitMessage
2025-10-28 16:44:49 +00:00
2025-10-28 16:42:56 +00:00
class BouncyBotSemiLite(BouncyBot):
2025-10-28 16:44:49 +00:00
highlight = (1, 1, 0.8)
2025-10-28 16:42:56 +00:00
punchiness = 0.85
run = False
default_boxing_gloves = False
charge_dist_min = 5
points_mult = 1
class FroshBot(SpazBot):
2025-10-28 16:44:49 +00:00
# Slightly beefier version of BomberBots, with slightly altered behaviour.
color = (0.13, 0.13, 0.13)
highlight = (0.2, 1, 1)
2025-10-28 16:42:56 +00:00
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
2025-10-28 16:44:49 +00:00
2025-10-28 16:42:56 +00:00
class FroshBotShielded(FroshBot):
default_shields = True
2025-10-28 16:44:49 +00:00
2025-10-28 16:42:56 +00:00
class StickyBotShielded(StickyBot):
default_shields = True
2025-10-28 16:44:49 +00:00
2025-10-28 16:42:56 +00:00
class IcePuncherBot(SpazBot2):
2025-10-28 16:44:49 +00:00
# A bot who can freeze anyone on punch and is immune to freezing.
color = (0, 0, 1)
highlight = (.2, .2, 1)
2025-10-28 16:42:56 +00:00
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):
2025-10-28 16:44:49 +00:00
# Add the ability to freeze anyone on punch.
2025-10-28 16:42:56 +00:00
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):
2025-10-28 16:44:49 +00:00
pass # resistant to freezing.
2025-10-28 16:42:56 +00:00
else:
return super().handlemessage(msg)
2025-10-28 16:44:49 +00:00
2025-10-28 16:42:56 +00:00
class IcePuncherBotShielded(IcePuncherBot):
default_shields = True
class GliderBot(BrawlerBot):
2025-10-28 16:44:49 +00:00
# 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)
2025-10-28 16:42:56 +00:00
character = 'B-9000'
charge_speed_min = 0.3
charge_speed_max = 1.0
def __init__(self) -> None:
super().__init__()
2025-10-28 16:44:49 +00:00
self.node.hockey = True # Added the ability to move fast "like in hockey".
2025-10-28 16:42:56 +00:00
class GliderBotShielded(GliderBot):
2025-10-28 16:44:49 +00:00
# Shielded version of GliderBot.
2025-10-28 16:42:56 +00:00
default_shields = True
class TeamBotSet(SpazBotSet):
"""Bots that can gather in a team and kill other bots."""
activity: BotsVSBotsGame
2025-10-28 16:44:49 +00:00
def __init__(self, team: Team) -> None:
2025-10-28 16:42:56 +00:00
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
2025-10-28 16:44:49 +00:00
def yell(self) -> None:
2025-10-28 16:42:56 +00:00
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,
2025-10-28 16:44:49 +00:00
]
2025-10-28 16:42:56 +00:00
if self._punching_bots_only == False:
bot_types += [
SpazBot,
StickyBot,
]
if self._pro_bots == True:
bot_types += [
BrawlerBotProShielded,
ChargerBotProShielded,
BouncyBot,
2025-10-28 16:44:49 +00:00
]
2025-10-28 16:42:56 +00:00
if self._punching_bots_only == False:
bot_types += [
BomberBotProShielded,
2025-10-28 16:44:49 +00:00
]
2025-10-28 16:42:56 +00:00
if self._bonus_bots == True:
bot_types += [
IcePuncherBot,
GliderBot,
]
if self._punching_bots_only == False:
bot_types += [
FroshBot,
2025-10-28 16:44:49 +00:00
]
2025-10-28 16:42:56 +00:00
if self._bonus_bots and self._pro_bots == True:
bot_types += [
IcePuncherBotShielded,
GliderBotShielded,
]
if self._punching_bots_only == False:
bot_types += [
FroshBotShielded,
StickyBotShielded,
2025-10-28 16:44:49 +00:00
]
return random.choice(bot_types)
2025-10-28 16:42:56 +00:00
@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'
2025-10-28 16:44:49 +00:00
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
2025-10-28 16:42:56 +00:00
@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)
2025-10-28 16:44:49 +00:00
@override
2025-10-28 16:42:56 +00:00
@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.'
2025-10-28 16:44:49 +00:00
2025-10-28 16:42:56 +00:00
@override
def get_instance_description_short(self) -> str | Sequence:
return 'Enjoy the battle.'
2025-10-28 16:44:49 +00:00
2025-10-28 16:42:56 +00:00
@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
2025-10-28 16:44:49 +00:00
# def team_pois_set
2025-10-28 16:42:56 +00:00
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(),
2025-10-28 16:44:49 +00:00
(camp[0], camp[1] + 1.8, random.randrange(-4, 5)),
self.spawn_time, team.bots.colorforteam)
2025-10-28 16:42:56 +00:00
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)