diff --git a/plugins/minigames.json b/plugins/minigames.json index ad74e4a..0946122 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -311,6 +311,151 @@ "md5sum": "4eb4a07830e4bd52fd19150a593d70c5" } } - } + }, + "arms_race": { + "description": "Upgrade your weapons by eliminating enemies.Win by being first one to kill while cursed", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "[Just] Freak#4999" + } + ], + "versions": { + "1.0.0": null + } + }, + "collector": { + "description": "Kill your opponents to steal their Capsules.Collect them and score at the Deposist Point!", + "external_url": "", + "authors": [ + { + "name": "TheMikirog", + "email": "", + "discord": "TheMikirog#1984" + }, + { + "name": "JoseAng3l", + "email": "", + "discord": "! JoseANG3L#0268" + } + ], + "versions": { + "1.0.0": null + } + }, + "dodge_the_ball": { + "description": "Survive from shooting balls", + "external_url": "", + "authors": [ + { + "name": "EmperoR", + "email": "", + "discord": "EmperoR#4098" + } + ], + "versions": { + "1.0.0": null + } + }, + "invisible_one": { + "description": "Be the invisible one for a length of time to win.Kill the invisible one to become it.", + "external_url": "", + "authors": [ + { + "name": "itsre3", + "email": "", + "discord": "itsre3#0267" + } + ], + "versions": { + "1.0.0": null + } + }, + "last_punch_stand": { + "description": "", + "external_url": "Last one to punch the spaz when timer ends wins", + "authors": [ + { + "name": "ThePersonMan", + "email": "", + "discord": "ThePersonMan#0276" + } + ], + "versions": { + "1.0.0": null + } + }, + "quake": { + "description": "Kill set number of enemies to win with optional rocket/railgun weapons", + "external_url": "", + "authors": [ + { + "name": "Dliwk", + "email": "", + "discord": "Dliwk#7961" + } + ], + "versions": { + "1.0.0": null + } + }, + "sleep_race": { + "description": "Can you win while sleeping?", + "external_url": "", + "authors": [ + { + "name": "itsre3", + "email": "", + "discord": "itsre3#0267" + } + ], + "versions": { + "1.0.0": null + } + }, + "snake": { + "description": "Survive a set number of mines to win", + "external_url": "", + "authors": [ + { + "name": "Sebastian", + "email": "", + "discord": "SEBASTIAN2059#5751" + } + ], + "versions": { + "1.0.0": null + } + }, + "ufo_fight": { + "description": "Fight the UFO boss!", + "external_url": "", + "authors": [ + { + "name": "Cross Joy", + "email": "cross.joy.official@gmail.com", + "discord": "Cross Joy#0721" + } + ], + "versions": { + "1.0.0": null + } + }, + "yeeting_party": { + "description": "Yeet your enemies out of the map!", + "external_url": "", + "authors": [ + { + "name": "Freaku", + "email": "", + "discord": "[Just] Freak#4999" + } + ], + "versions": { + "1.0.0": null + } + }, } -} \ No newline at end of file +} diff --git a/plugins/minigames/arms_race.py b/plugins/minigames/arms_race.py new file mode 100644 index 0000000..13f32b7 --- /dev/null +++ b/plugins/minigames/arms_race.py @@ -0,0 +1,194 @@ +#Ported by: Freaku / @[Just] Freak#4999 + +#Join BCS: +# https://discord.gg/ucyaesh + + + +# ba_meta require api 7 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + + + +class State: + def __init__(self, bomb=None, grab=False, punch=False, curse=False, required=False, final=False, name=''): + self.bomb = bomb + self.grab = grab + self.punch = punch + self.pickup = False + self.curse = curse + self.required = required or final + self.final = final + self.name = name + self.next = None + self.index = None + + def apply(self, spaz): + spaz.disconnect_controls_from_player() + spaz.connect_controls_to_player(enable_punch=self.punch, + enable_bomb=self.bomb, + enable_pickup=self.grab) + if self.curse: + spaz.curse_time = -1 + spaz.curse() + if self.bomb: + spaz.bomb_type = self.bomb + spaz.set_score_text(self.name) + + def get_setting(self): + return (self.name) + + +states = [ State(bomb='normal', name='Basic Bombs'), + State(bomb='ice', name='Frozen Bombs'), + State(bomb='sticky', name='Sticky Bombs'), + State(bomb='impact', name='Impact Bombs'), + State(grab=True, name='Grabbing only'), + State(punch=True, name='Punching only'), + State(curse=True, name='Cursed', final=True) ] + +class Player(ba.Player['Team']): + """Our player type for this game.""" + def __init__(self): + self.state = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class ArmsRaceGame(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Arms Race' + description = 'Upgrade your weapon by eliminating enemies.\nWin the match by being the first player\nto get a kill while cursed.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False)] + for state in states: + if not state.required: + settings.append(ba.BoolSetting(state.get_setting(), default=True)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self.states = [s for s in states if settings.get(s.name, True)] + for i, state in enumerate(self.states): + if i < len(self.states) and not state.final: + state.next = self.states[i + 1] + state.index = i + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._time_limit = float(settings['Time Limit']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Upgrade your weapon by eliminating enemies.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies', len(self.states) + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + + def on_player_join(self, player): + if player.state is None: + player.state = self.states[0] + self.spawn_player(player) + + # overriding the default character spawning.. + def spawn_player(self, player): + if player.state is None: + player.state = self.states[0] + super().spawn_player(player) + player.state.apply(player.actor) + + def isValidKill(self, m): + if m.getkillerplayer(Player) is None: + return False + + if m.getkillerplayer(Player).team is m.getplayer(Player).team: + return False + + return True + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.PlayerDiedMessage): + if self.isValidKill(msg): + self.stats.player_scored(msg.getkillerplayer(Player), 10, kill=True) + if not msg.getkillerplayer(Player).state.final: + msg.getkillerplayer(Player).state = msg.getkillerplayer(Player).state.next + msg.getkillerplayer(Player).state.apply(msg.getkillerplayer(Player).actor) + else: + msg.getkillerplayer(Player).team.score += 1 + self.end_game() + self.respawn_player(msg.getplayer(Player)) + + else: + return super().handlemessage(msg) + return None + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/plugins/minigames/collector.py b/plugins/minigames/collector.py new file mode 100644 index 0000000..1a22bda --- /dev/null +++ b/plugins/minigames/collector.py @@ -0,0 +1,642 @@ +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +''' + Gamemode: Collector + Creator: TheMikirog + Website: https://bombsquadjoyride.blogspot.com/ + + This is a gamemode purely made by me just to spite unchallenged modders + out there that put out crap to the market. + We don't want gamemodes that are just the existing ones + with some novelties! Gamers deserve more! + + In this gamemode you have to kill others in order to get their Capsules. + Capsules can be collected and staked in your inventory, + how many as you please. + After you kill an enemy that carries some of them, + they drop a respective amount of Capsules they carried + two more. + Your task is to collect these Capsules, + get to the flag and score them KOTH style. + You can't score if you don't have any Capsules with you. + The first player or team to get to the required ammount wins. + This is a gamemode all about trying to stay alive + and picking your battles in order to win. + A rare skill in BombSquad, where everyone is overly aggressive. +''' + +from __future__ import annotations + +import weakref +from enum import Enum +from typing import TYPE_CHECKING + +import ba +import random +from bastd.actor.flag import Flag +from bastd.actor.popuptext import PopupText +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = ba.app.lang.language +if lang == 'Spanish': + name = 'Coleccionista' + description = ('Elimina a tus oponentes para robar sus cápsulas.\n' + '¡Recolecta y anota en el punto de depósito!') + description_ingame = 'Obtén ${ARG1} cápsulas de tus enemigos.' + description_short = 'colecciona ${ARG1} cápsulas' + tips = [( + '¡Si tu oponente cae fuera del mapa, sus cápsulas desapareceran!\n' + 'No intestes matar a tus enemigos arrojándolos al vacio.'), + 'No te apresures. ¡Puedes perder tus cápsulas rápidamente!', + ('¡No dejes que el jugador con más cápsulas anote!\n' + '¡Intenta atraparlo si puedes!'), + ('¡Las Capsulas de la Suerte te dan 4 cápsulas en lugar de 2' + 'y tienen un 8% de probabilidad de aparecer después de matar'), + ('¡No te quedes en un solo lugar! Muevete más rapido que tu enemigo, ' + '¡con suerte conseguirás algunas cápsulas!'), + ] + capsules_to_win = 'Cápsulas para Ganar' + capsules_death = 'Cápsulas al Morir' + lucky_capsules = 'Cápsulas de la Suerte' + bonus = '¡BONUS!' + full_capacity = '¡Capacidad Completa!' +else: + name = 'Collector' + description = ('Kill your opponents to steal their Capsules.\n' + 'Collect them and score at the Deposit point!') + description_ingame = 'Score ${ARG1} capsules from your enemies.' + description_short = 'collect ${ARG1} capsules' + tips = [( + 'Making you opponent fall down the pit makes his Capsules wasted!\n' + 'Try not to kill enemies by throwing them off the cliff.'), + 'Don\'t be too reckless. You can lose your loot quite quickly!', + ('Don\'t let the leading player score his Capsules ' + 'at the Deposit Point!\nTry to catch him if you can!'), + ('Lucky Capsules give 4 to your inventory and they have 8% chance ' + 'of spawning after kill!'), + ('Don\'t camp in one place! Make your move first, ' + 'so hopefully you get some dough!'), + ] + capsules_to_win = 'Capsules to Win' + capsules_death = 'Capsules on Death' + lucky_capsules = 'Allow Lucky Capsules' + bonus = 'BONUS!' + full_capacity = 'Full Capacity!' + + +class FlagState(Enum): + """States our single flag can be in.""" + + NEW = 0 + UNCONTESTED = 1 + CONTESTED = 2 + HELD = 3 + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.time_at_flag = 0 + self.capsules = 0 + self.light = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class CollectorGame(ba.TeamGameActivity[Player, Team]): + + name = name + description = description + tips = tips + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[ba.Session] + ) -> list[ba.Setting]: + settings = [ + ba.IntSetting( + capsules_to_win, + min_value=1, + default=10, + increment=1, + ), + ba.IntSetting( + capsules_death, + min_value=1, + max_value=10, + default=2, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting(lucky_capsules, default=True), + ba.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) or issubclass( + sessiontype, ba.FreeForAllSession + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ba.getmaps('keep_away') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._score_to_win: int | None = None + self._swipsound = ba.getsound('swip') + self._lucky_sound = ba.getsound('ding') + + self._flag_pos: Sequence[float] | None = None + self._flag_state: FlagState | None = None + self._flag: Flag | None = None + self._flag_light: ba.Node | None = None + self._scoring_team: weakref.ref[Team] | None = None + self._time_limit = float(settings['Time Limit']) + self._epic_mode = bool(settings['Epic Mode']) + + self._capsules_to_win = int(settings[capsules_to_win]) + self._capsules_death = int(settings[capsules_death]) + self._lucky_capsules = bool(settings[lucky_capsules]) + self._capsules: list[Any] = [] + + self._capsule_model = ba.getmodel('bomb') + self._capsule_tex = ba.gettexture('bombColor') + self._capsule_lucky_tex = ba.gettexture('bombStickyColor') + self._collect_sound = ba.getsound('powerup01') + self._lucky_collect_sound = ba.getsound('cashRegister2') + + self._capsule_material = ba.Material() + self._capsule_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=('call', 'at_connect', self._on_capsule_player_collide), + ) + + self._flag_region_material = ba.Material() + self._flag_region_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ( + 'call', + 'at_connect', + ba.Call(self._handle_player_flag_region_collide, True), + ), + ( + 'call', + 'at_disconnect', + ba.Call(self._handle_player_flag_region_collide, False), + ), + ), + ) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = ( + ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SCARY + ) + + def get_instance_description(self) -> str | Sequence: + return description_ingame, self._score_to_win + + def get_instance_description_short(self) -> str | Sequence: + return description_short, self._score_to_win + + def create_team(self, sessionteam: ba.SessionTeam) -> Team: + return Team() + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + shared = SharedObjects.get() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = self._capsules_to_win * max( + 1, max(len(t.players) for t in self.teams) + ) + self._update_scoreboard() + + if isinstance(self.session, ba.FreeForAllSession): + self._flag_pos = self.map.get_flag_position(random.randint(0, 1)) + else: + self._flag_pos = self.map.get_flag_position(None) + + ba.timer(1.0, self._tick, repeat=True) + self._flag_state = FlagState.NEW + Flag.project_stand(self._flag_pos) + self._flag = Flag( + position=self._flag_pos, touchable=False, color=(1, 1, 1) + ) + self._flag_light = ba.newnode( + 'light', + attrs={ + 'position': self._flag_pos, + 'intensity': 0.2, + 'height_attenuated': False, + 'radius': 0.4, + 'color': (0.2, 0.2, 0.2), + }, + ) + # Flag region. + flagmats = [self._flag_region_material, shared.region_material] + ba.newnode( + 'region', + attrs={ + 'position': self._flag_pos, + 'scale': (1.8, 1.8, 1.8), + 'type': 'sphere', + 'materials': flagmats, + }, + ) + self._update_flag_state() + + def _tick(self) -> None: + self._update_flag_state() + + if self._scoring_team is None: + scoring_team = None + else: + scoring_team = self._scoring_team() + + if not scoring_team: + return + + if isinstance(self.session, ba.FreeForAllSession): + players = self.players + else: + players = scoring_team.players + + for player in players: + if player.time_at_flag > 0: + self.stats.player_scored( + player, 3, screenmessage=False, display=False + ) + if player.capsules > 0: + if self._flag_state != FlagState.HELD: + return + if scoring_team.score >= self._score_to_win: + return + + player.capsules -= 1 + scoring_team.score += 1 + self._handle_capsule_storage(( + self._flag_pos[0], + self._flag_pos[1]+1, + self._flag_pos[2] + ), player) + ba.playsound( + self._collect_sound, + 0.8, + position=self._flag_pos) + + self._update_scoreboard() + if player.capsules > 0: + assert self._flag is not None + self._flag.set_score_text( + str(self._score_to_win - scoring_team.score)) + + # winner + if scoring_team.score >= self._score_to_win: + self.end_game() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results, announce_delay=0) + + def _update_flag_state(self) -> None: + holding_teams = set( + player.team for player in self.players if player.time_at_flag + ) + prev_state = self._flag_state + assert self._flag_light + assert self._flag is not None + assert self._flag.node + if len(holding_teams) > 1: + self._flag_state = FlagState.CONTESTED + self._scoring_team = None + self._flag_light.color = (0.6, 0.6, 0.1) + self._flag.node.color = (1.0, 1.0, 0.4) + elif len(holding_teams) == 1: + holding_team = list(holding_teams)[0] + self._flag_state = FlagState.HELD + self._scoring_team = weakref.ref(holding_team) + self._flag_light.color = ba.normalized_color(holding_team.color) + self._flag.node.color = holding_team.color + else: + self._flag_state = FlagState.UNCONTESTED + self._scoring_team = None + self._flag_light.color = (0.2, 0.2, 0.2) + self._flag.node.color = (1, 1, 1) + if self._flag_state != prev_state: + ba.playsound(self._swipsound) + + def _handle_player_flag_region_collide(self, colliding: bool) -> None: + try: + spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True) + except ba.NotFoundError: + return + + if not spaz.is_alive(): + return + + player = spaz.getplayer(Player, True) + + # Different parts of us can collide so a single value isn't enough + # also don't count it if we're dead (flying heads shouldn't be able to + # win the game :-) + if colliding and player.is_alive(): + player.time_at_flag += 1 + else: + player.time_at_flag = max(0, player.time_at_flag - 1) + + self._update_flag_state() + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value( + team, team.score, self._score_to_win + ) + + def _drop_capsule(self, player: Player) -> None: + pt = player.node.position + + # Throw out capsules that the victim has + 2 more to keep the game running + for i in range(player.capsules + self._capsules_death): + # How far from each other these capsules should spawn + w = 0.6 + # How much these capsules should fly after spawning + s = 0.005 - (player.capsules * 0.01) + self._capsules.append( + Capsule( + position=(pt[0] + random.uniform(-w, w), + pt[1] + 0.75 + random.uniform(-w, w), + pt[2]), + velocity=(random.uniform(-s, s), + random.uniform(-s, s), + random.uniform(-s, s)), + lucky=False)) + if random.randint(1, 12) == 1 and self._lucky_capsules: + # How far from each other these capsules should spawn + w = 0.6 + # How much these capsules should fly after spawning + s = 0.005 + self._capsules.append( + Capsule( + position=(pt[0] + random.uniform(-w, w), + pt[1] + 0.75 + random.uniform(-w, w), + pt[2]), + velocity=(random.uniform(-s, s), + random.uniform(-s, s), + random.uniform(-s, s)), + lucky=True)) + + def _on_capsule_player_collide(self) -> None: + if self.has_ended(): + return + collision = ba.getcollision() + + # Be defensive here; we could be hitting the corpse of a player + # who just left/etc. + try: + capsule = collision.sourcenode.getdelegate(Capsule, True) + player = collision.opposingnode.getdelegate( + PlayerSpaz, True + ).getplayer(Player, True) + except ba.NotFoundError: + return + + if not player.is_alive(): + return + + if capsule.node.color_texture == self._capsule_lucky_tex: + player.capsules += 4 + PopupText( + bonus, + color=(1, 1, 0), + scale=1.5, + position=capsule.node.position + ).autoretain() + ba.playsound( + self._lucky_collect_sound, + 1.0, + position=capsule.node.position) + ba.emitfx( + position=capsule.node.position, + velocity=(0, 0, 0), + count=int(6.4+random.random()*24), + scale=1.2, + spread=2.0, + chunk_type='spark'); + ba.emitfx( + position=capsule.node.position, + velocity=(0, 0, 0), + count=int(4.0+random.random()*6), + emit_type='tendrils'); + else: + player.capsules += 1 + ba.playsound( + self._collect_sound, + 0.6, + position=capsule.node.position) + # create a flash + light = ba.newnode( + 'light', + attrs={ + 'position': capsule.node.position, + 'height_attenuated': False, + 'radius': 0.1, + 'color': (1, 1, 0)}) + + # Create a short text informing about your inventory + self._handle_capsule_storage(player.position, player) + + ba.animate(light, 'intensity', { + 0: 0, + 0.1: 0.5, + 0.2: 0 + }, loop=False) + ba.timer(0.2, light.delete) + capsule.handlemessage(ba.DieMessage()) + + def _update_player_light(self, player: Player, capsules: int) -> None: + if player.light: + intensity = 0.04 * capsules + ba.animate(player.light, 'intensity', { + 0.0: player.light.intensity, + 0.1: intensity + }) + def newintensity(): + player.light.intensity = intensity + ba.timer(0.1, newintensity) + else: + player.light = ba.newnode( + 'light', + attrs={ + 'height_attenuated': False, + 'radius': 0.2, + 'intensity': 0.0, + 'color': (0.2, 1, 0.2) + }) + player.node.connectattr('position', player.light, 'position') + + def _handle_capsule_storage(self, pos: float, player: Player) -> None: + capsules = player.capsules + text = str(capsules) + scale = 1.75 + (0.02 * capsules) + if capsules > 10: + player.capsules = 10 + text = full_capacity + color = (1, 0.85, 0) + elif capsules > 7: + color = (1, 0, 0) + scale = 2.4 + elif capsules > 5: + color = (1, 0.4, 0.4) + scale = 2.1 + elif capsules > 3: + color = (1, 1, 0.4) + scale = 2.0 + else: + color = (1, 1, 1) + scale = 1.9 + PopupText( + text, + color=color, + scale=scale, + position=(pos[0], pos[1]-1, pos[2]) + ).autoretain() + self._update_player_light(player, capsules) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) # Augment default. + # No longer can count as time_at_flag once dead. + player = msg.getplayer(Player) + player.time_at_flag = 0 + self._update_flag_state() + self._drop_capsule(player) + player.capsules = 0 + self._update_player_light(player, 0) + self.respawn_player(player) + else: + return super().handlemessage(msg) + + +class Capsule(ba.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.5, 0.0), + lucky: bool = False): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # spawn just above the provided point + self._spawn_pos = (position[0], position[1], position[2]) + + if lucky: + ba.playsound(activity._lucky_sound, 1.0, self._spawn_pos) + + self.node = ba.newnode( + 'prop', + attrs={ + 'model': activity._capsule_model, + 'color_texture': activity._capsule_lucky_tex if lucky else ( + activity._capsule_tex), + 'body': 'crate' if lucky else 'capsule', + 'reflection': 'powerup' if lucky else 'soft', + 'body_scale': 0.65 if lucky else 0.3, + 'density':6.0 if lucky else 4.0, + 'reflection_scale': [0.15], + 'shadow_size': 0.65 if lucky else 0.6, + 'position': self._spawn_pos, + 'velocity': velocity, + 'materials': [ + shared.object_material, activity._capsule_material] + }, + delegate=self) + ba.animate(self.node, 'model_scale', { + 0.0: 0.0, + 0.1: 0.9 if lucky else 0.6, + 0.16: 0.8 if lucky else 0.5 + }) + self._light_capsule = ba.newnode( + 'light', + attrs={ + 'position': self._spawn_pos, + 'height_attenuated': False, + 'radius': 0.5 if lucky else 0.1, + 'color': (0.2, 0.2, 0) if lucky else (0.2, 1, 0.2) + }) + self.node.connectattr('position', self._light_capsule, 'position') + + def handlemessage(self, msg: Any): + if isinstance(msg, ba.DieMessage): + self.node.delete() + ba.animate(self._light_capsule, 'intensity', { + 0: 1.0, + 0.05: 0.0 + }, loop=False) + ba.timer(0.05, self._light_capsule.delete) + elif isinstance(msg, ba.OutOfBoundsMessage): + self.handlemessage(ba.DieMessage()) + elif isinstance(msg, ba.HitMessage): + self.node.handlemessage( + 'impulse', + msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0]/8, msg.velocity[1]/8, msg.velocity[2]/8, + 1.0*msg.magnitude, 1.0*msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + else: + return super().handlemessage(msg) diff --git a/plugins/minigames/dodge_the_ball.py b/plugins/minigames/dodge_the_ball.py new file mode 100644 index 0000000..d5b1770 --- /dev/null +++ b/plugins/minigames/dodge_the_ball.py @@ -0,0 +1,762 @@ +""" + + DondgeTheBall minigame by EmperoR#4098 + +""" + +# Feel free to edit. + +# ba_meta require api 7 +from __future__ import annotations +from typing import TYPE_CHECKING + +import ba +from random import choice +from enum import Enum +from bastd.actor.bomb import Blast +from bastd.actor.popuptext import PopupText +from bastd.actor.powerupbox import PowerupBox +from bastd.actor.onscreencountdown import OnScreenCountdown +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import NoReturn, Sequence, Any + + +# Type of ball in this game +class BallType(Enum): + """ Types of ball """ + EASY = 0 + # Decrease the next ball shooting speed(not ball speed). + # Ball color is yellow. + MEDIUM = 1 + # increase the next ball shooting speed(not ball speed). + # target the head of player. + # Ball color is purple. + HARD = 2 + # Target player according to player movement (not very accurate). + # Taget: player head. + # increase the next ball speed but less than MEDIUM. + # Ball color is crimson(purple+red = pinky color type). + +# this dict decide the ball_type spawning rate like powerup box +ball_type_dict: dict[BallType, int] = { + BallType.EASY: 3, + BallType.MEDIUM: 2, + BallType.HARD: 1, +}; + +class Ball(ba.Actor): + """ Shooting Ball """ + def __init__(self, + position: Sequence[float], + velocity: Sequence[float], + texture: ba.Texture, + body_scale: float = 1.0, + gravity_scale: float = 1.0, + ) -> NoReturn: + + super().__init__(); + + shared = SharedObjects.get(); + + ball_material = ba.Material(); + ball_material.add_actions( + conditions=( + ( + ('we_are_younger_than', 100), + 'or', + ('they_are_younger_than', 100), + ), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ); + + self.node = ba.newnode( + 'prop', + delegate=self, + attrs={ + 'body': 'sphere', + 'position': position, + 'velocity': velocity, + 'body_scale': body_scale, + 'model': ba.getmodel('frostyPelvis'), + 'model_scale': body_scale, + 'color_texture': texture, + 'gravity_scale': gravity_scale, + 'density': 4.0, # increase density of ball so ball collide with player with heavy force. # ammm very bad grammer + 'materials': (ball_material,), + }, + ); + + # die the ball manually incase the ball doesn't fall the outside of the map + ba.timer(2.5, ba.WeakCall(self.handlemessage, ba.DieMessage())); + + # i am not handling anything in this ball Class(except for diemessage). + # all game things and logics going to be in the box class + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.DieMessage): + self.node.delete(); + else: + super().handlemessage(msg); + + +class Box(ba.Actor): + """ A box that spawn midle of map as a decoration perpose """ + + def __init__(self, + position: Sequence[float], + velocity: Sequence[float], + ) -> NoReturn: + + super().__init__(); + + shared = SharedObjects.get(); + # self.ball_jump = 0.0; + no_hit_material = ba.Material(); + # we don't need that the box was move and collide with objects. + no_hit_material.add_actions( + conditions=( + ('they_have_material', shared.pickup_material), + 'or', + ('they_have_material', shared.attack_material), + ), + actions=('modify_part_collision', 'collide', False), + ); + + no_hit_material.add_actions( + conditions=( + ('they_have_material', shared.object_material), + 'or', + ('they_dont_have_material', shared.footing_material), + ), + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False), + ), + ); + + self.node = ba.newnode( + 'prop', + delegate=self, + attrs={ + 'body': 'box', + 'position': position, + 'model': ba.getmodel('powerup'), + 'light_model': ba.getmodel('powerupSimple'), + 'shadow_size': 0.5, + 'body_scale': 1.4, + 'model_scale': 1.4, + 'color_texture': ba.gettexture('landMineLit'), + 'reflection': 'powerup', + 'reflection_scale': [1.0], + 'materials': (no_hit_material,), + }, + ); + # light + self.light = ba.newnode( + "light", + owner = self.node, + attrs={ + 'radius' : 0.2, + 'intensity' : 0.8, + 'color': (0.0, 1.0, 0.0), + } + ); + self.node.connectattr("position", self.light, "position"); + # Drawing circle and circleOutline in radius of 3, + # so player can see that how close he is to the box. + # If player is inside this circle the ball speed will increase. + circle = ba.newnode( + "locator", + owner = self.node, + attrs = { + 'shape': 'circle', + 'color': (1.0, 0.0, 0.0), + 'opacity': 0.1, + 'size': (6.0, 0.0, 6.0), + 'draw_beauty': False, + 'additive': True, + }, + ) + self.node.connectattr("position", circle, "position"); + # also adding a outline cause its look nice. + circle_outline = ba.newnode( + "locator", + owner = self.node, + attrs = { + 'shape': 'circleOutline', + 'color': (1.0, 1.0, 0.0), + 'opacity': 0.1, + 'size': (6.0, 0.0, 6.0), + 'draw_beauty': False, + 'additive': True, + }, + ); + self.node.connectattr("position", circle_outline, "position"); + + # all ball attribute that we need. + self.ball_type: BallType = BallType.EASY; + self.shoot_timer: ba.Timer | None = None; + self.shoot_speed: float = 0.0; + # this force the shoot if player is inside the red circle. + self.force_shoot_speed: float = 0.0; + self.ball_mag = 3000; + self.ball_gravity: float = 1.0; + self.ball_tex: ba.Texture | None = None; + # only for Hard ball_type + self.player_facing_direction: list[float, float] = [0.0, 0.0]; + # ball shoot soound. + self.shoot_sound = ba.getsound('laserReverse'); + + # same as "powerupdist" + self.ball_type_dist: list[BallType] = []; + + for ball in ball_type_dict: + for _ in range(ball_type_dict[ball]): + self.ball_type_dist.append(ball); + + # Here main logic of game goes here. + # like shoot balls, shoot speed, anything we want goes here(except for some thing). + def start_shoot(self) -> NoReturn: + + # getting all allive players in a list. + alive_players_list = self.activity.get_alive_players(); + + # make sure that list is not Empty. + if len(alive_players_list) > 0: + + # choosing a random player from list. + target_player = choice(alive_players_list); + # highlight the target player + self.highlight_target_player(target_player); + + # to finding difference between player and box. + # we just need to subtract player pos and ball pos. + # Same logic as eric applied in Target Practice Gamemode. + difference = ba.Vec3(target_player.position) - ba.Vec3(self.node.position); + + # discard Y position so ball shoot more straight. + difference[1] = 0.0 + + # and now, this length method returns distance in float. + # we're gonna use this value for calculating player analog stick + distance = difference.length(); + + # shoot a random BallType + self.upgrade_ball_type(choice(self.ball_type_dist)); + + # and check the ball_type and upgrade it gravity_scale, texture, next ball speed. + self.check_ball_type(self.ball_type); + + # For HARD ball i am just focusing on player analog stick facing direction. + # Not very accurate and that's we need. + if self.ball_type == BallType.HARD: + self.calculate_player_analog_stick(target_player, distance); + else: + self.player_facing_direction = [0.0, 0.0]; + + pos = self.node.position; + + if self.ball_type == BallType.MEDIUM or self.ball_type == BallType.HARD: + # Target head by increasing Y pos. + # How this work? cause ball gravity_scale is ...... + pos = (pos[0], pos[1]+.25, pos[2]); + + # ball is generating.. + ball = Ball( + position = pos, + velocity = (0.0, 0.0, 0.0), + texture = self.ball_tex, + gravity_scale = self.ball_gravity, + body_scale = 1.0, + ).autoretain(); + + # shoot Animation and sound. + self.shoot_animation(); + + # force the shoot speed if player try to go inside the red circle. + if self.force_shoot_speed != 0.0: + self.shoot_speed = self.force_shoot_speed; + + # push the ball to the player + ball.node.handlemessage( + 'impulse', + self.node.position[0], # ball spawn position X + self.node.position[1], # Y + self.node.position[2], # Z + 0, 0, 0, # velocity x,y,z + self.ball_mag, # magnetude + 0.000, # magnetude velocity + 0.000, # radius + 0.000, # idk + difference[0] + self.player_facing_direction[0], # force direction X + difference[1] , # force direction Y + difference[2] + self.player_facing_direction[1], # force direction Z + ); + # creating our timer and shoot the ball again.(and we create a loop) + self.shoot_timer = ba.Timer(self.shoot_speed, self.start_shoot); + + def upgrade_ball_type(self, ball_type: BallType) -> NoReturn: + + self.ball_type = ball_type; + + def check_ball_type(self, ball_type: BallType) -> NoReturn: + + if ball_type == BallType.EASY: + self.shoot_speed = 0.8; + self.ball_gravity = 1.0; + # next ball shoot speed + self.ball_mag = 3000; + # box light color and ball tex + self.light.color = (1.0, 1.0, 0.0); + self.ball_tex = ba.gettexture('egg4'); + elif ball_type == BallType.MEDIUM: + self.ball_mag = 3000; + # decrease the gravity scale so, ball shoot without falling and straight. + self.ball_gravity = 0.0; + # next ball shoot speed. + self.shoot_speed = 0.4; + # box light color and ball tex. + self.light.color = (1.0, 0.0, 1.0); + self.ball_tex = ba.gettexture('egg3'); + elif ball_type == BallType.HARD: + self.ball_mag = 2500; + self.ball_gravity = 0.0; + # next ball shoot speed. + self.shoot_speed = 0.6; + # box light color and ball tex. + self.light.color = (1.0, 0.2, 1.0); + self.ball_tex = ba.gettexture('egg1'); + + def shoot_animation(self) -> NoReturn: + + ba.animate( + self.node, + "model_scale",{ + 0.00: 1.4, + 0.05: 1.7, + 0.10: 1.4, + } + ); + # playing shoot sound. + ba.playsound(self.shoot_sound, position = self.node.position); + + def highlight_target_player(self, player: ba.Player) -> NoReturn: + + # adding light + light = ba.newnode( + "light", + owner = self.node, + attrs={ + 'radius':0.0, + 'intensity':1.0, + 'color': (1.0, 0.0, 0.0), + } + ); + ba.animate( + light, + "radius",{ + 0.05: 0.02, + 0.10: 0.07, + 0.15: 0.15, + 0.20: 0.13, + 0.25: 0.10, + 0.30: 0.05, + 0.35: 0.02, + 0.40: 0.00, + } + ); + # And a circle outline with ugly animation. + circle_outline = ba.newnode( + "locator", + owner = player.actor.node, + attrs={ + 'shape': 'circleOutline', + 'color': (1.0, 0.0, 0.0), + 'opacity': 1.0, + 'draw_beauty': False, + 'additive': True, + }, + ); + ba.animate_array( + circle_outline, + 'size', + 1, { + 0.05: [0.5], + 0.10: [0.8], + 0.15: [1.5], + 0.20: [2.0], + 0.25: [1.8], + 0.30: [1.3], + 0.35: [0.6], + 0.40: [0.0], + } + ); + + # coonect it and... + player.actor.node.connectattr("position", light, "position"); + player.actor.node.connectattr("position", circle_outline, "position"); + + # immediately delete the node after another player has been targeted. + self.shoot_speed = 0.5 if self.shoot_speed == 0.0 else self.shoot_speed; + ba.timer(self.shoot_speed, light.delete); + ba.timer(self.shoot_speed, circle_outline.delete); + + def calculate_player_analog_stick(self, player:ba.Player, distance: float) -> NoReturn: + # at first i was very confused how i can read the player analog stick \ + # then i saw TheMikirog#1984 autorun plugin code. + # and i got it how analog stick values are works. + # just need to store analog stick facing direction and need some calculation according how far player pushed analog stick. + # Notice that how vertical direction is inverted, so we need to put a minus infront of veriable.(so ball isn't shoot at wrong direction). + self.player_facing_direction[0] = player.actor.node.move_left_right; + self.player_facing_direction[1] = -player.actor.node.move_up_down; + + # if player is too close and the player pushing his analog stick fully the ball shoot's too far away to player. + # so, we need to reduce the value of "self.player_facing_direction" to fix this problem. + if distance <= 3: + self.player_facing_direction[0] = 0.4 if self.player_facing_direction[0] > 0 else -0.4; + self.player_facing_direction[1] = 0.4 if self.player_facing_direction[0] > 0 else -0.4; + # same problem to long distance but in reverse, the ball can't reach to the player, + # its because player analog stick value is between 1 and -1, + # and this value is low to shoot ball forward to Player if player is too far from the box. + # so. let's increase to 1.5 if player pushed analog stick fully. + elif distance > 6.5: + # So many calculation according to how analog stick pushed by player. + # Horizontal(left-right) calculation + if self.player_facing_direction[0] > 0.4: + self.player_facing_direction[0] = 1.5; + elif self.player_facing_direction[0] < -0.4: + self.player_facing_direction[0] = -1.5; + else: + if self.player_facing_direction[0] > 0.0: + self.player_facing_direction[0] = 0.2; + elif self.player_facing_direction[0] < 0.0: + self.player_facing_direction[0] = -0.2; + else: + self.player_facing_direction[0] = 0.0; + + # Vertical(up-down) calculation. + if self.player_facing_direction[1] > 0.4: + self.player_facing_direction[1] = 1.5; + elif self.player_facing_direction[1] < -0.4: + self.player_facing_direction[1] = -1.5; + else: + if self.player_facing_direction[1] > 0.0: + self.player_facing_direction[1] = 0.2; + elif self.player_facing_direction[1] < 0.0: + self.player_facing_direction[1] = -0.2; + else: + self.player_facing_direction[1] = -0.0; + + # if we want stop the ball shootes + def stop_shoot(self) -> NoReturn: + # Kill the timer. + self.shoot_timer = None; + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + +# almost 80 % for game we done in box class. +# now remain things, like name, seetings, scoring, cooldonw, +# and main thing don't allow player to camp inside of box are going in this class. + +# ba_meta export game +class DodgeTheBall(ba.TeamGameActivity[Player, Team]): + + # defining name, description and settings.. + name = 'Dodge the ball'; + description = 'Survive from shooting balls'; + + available_settings = [ + ba.IntSetting( + 'Cooldown', + min_value = 20, + default = 45, + increment = 5, + ), + ba.BoolSetting('Epic Mode', default=False) + ] + + # Don't allow joining after we start. + allow_mid_activity_joins = False; + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + # We support team and ffa sessions. + return issubclass(sessiontype, ba.FreeForAllSession) or issubclass( + sessiontype, ba.DualTeamSession, + ); + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + # This Game mode need a flat and perfect shape map where can player fall outside map. + # bombsquad have "Doom Shroom" map. + # Not perfect map for this game mode but its fine for this gamemode. + # the problem is that Doom Shroom is not a perfect circle and not flat also. + return ['Doom Shroom']; + + def __init__(self, settings: dict): + super().__init__(settings); + self._epic_mode = bool(settings['Epic Mode']); + self.countdown_time = int(settings['Cooldown']); + + self.check_player_pos_timer: ba.Timer | None = None; + self.shield_drop_timer: ba.Timer | None = None; + # cooldown and Box + self._countdown: OnScreenCountdown | None = None; + self.box: Box | None = None; + + # this lists for scoring. + self.joined_player_list: list[ba.Player] = []; + self.dead_player_list: list[ba.Player] = []; + + # normally play RUN AWAY music cause is match with our gamemode at.. my point, + # but in epic switch to EPIC. + self.slow_motion = self._epic_mode; + self.default_music = ( + ba.MusicType.EPIC if self._epic_mode else ba.MusicType.RUN_AWAY + ); + + def get_instance_description(self) -> str | Sequence: + return 'Keep away as possible as you can'; + + # add a tiny text under our game name. + def get_instance_description_short(self) -> str | Sequence: + return 'Dodge the shooting balls'; + + def on_begin(self) -> NoReturn: + super().on_begin(); + + # spawn our box at middle of the map + self.box = Box( + position=(0.5, 2.7, -3.9), + velocity=(0.0, 0.0, 0.0), + ).autoretain(); + + # create our cooldown + self._countdown = OnScreenCountdown( + duration = self.countdown_time, + endcall = self.play_victory_sound_and_end, + ); + + # and starts the cooldown and shootes. + ba.timer(5.0, self._countdown.start); + ba.timer(5.0, self.box.start_shoot); + + # start checking all player pos. + ba.timer(5.0, self.check_player_pos); + + # drop shield every ten Seconds + # need five seconds delay Because shootes start after 5 seconds. + ba.timer(15.0, self.drop_shield); + + # This function returns all alive players in game. + # i thinck you see this function in Box class. + def get_alive_players(self) -> Sequence[ba.Player]: + + alive_players = []; + + for team in self.teams: + for player in team.players: + if player.is_alive(): + alive_players.append(player); + + return alive_players; + + # let's disallowed camping inside of box by doing a blast and increasing ball shoot speed. + def check_player_pos(self): + + for player in self.get_alive_players(): + + # same logic as applied for the ball + difference = ba.Vec3(player.position) - ba.Vec3(self.box.node.position); + + distance = difference.length(); + + if distance < 3: + self.box.force_shoot_speed = 0.2; + else: + self.box.force_shoot_speed = 0.0; + + if distance < 0.5: + Blast( + position = self.box.node.position, + velocity = self.box.node.velocity, + blast_type = 'normal', + blast_radius = 1.0, + ).autoretain(); + + PopupText( + position = self.box.node.position, + text = 'Keep away from me', + random_offset = 0.0, + scale = 2.0, + color = self.box.light.color, + ).autoretain(); + + # create our timer and start looping it + self.check_player_pos_timer = ba.Timer(0.1, self.check_player_pos); + + # drop useless shield's too give player temptation. + def drop_shield(self) -> NoReturn: + + pos = self.box.node.position; + + PowerupBox( + position = (pos[0] + 4.0, pos[1] + 3.0, pos[2]), + poweruptype = 'shield', + ).autoretain(); + + PowerupBox( + position = (pos[0] - 4.0, pos[1] + 3.0, pos[2]), + poweruptype = 'shield', + ).autoretain(); + + self.shield_drop_timer = ba.Timer(10.0, self.drop_shield); + + # when cooldown time up i don't want that the game end immediately. + def play_victory_sound_and_end(self) -> NoReturn: + + # kill timers + self.box.stop_shoot(); + self.check_player_pos_timer = None + self.shield_drop_timer = None + + ba.timer(2.0, self.end_game); + + # this function runs when A player spawn in map + def spawn_player(self, player: Player) -> NoReturn: + spaz = self.spawn_player_spaz(player); + + # reconnect this player's controls. + # without bomb, punch and pickup. + spaz.connect_controls_to_player( + enable_punch=False, enable_bomb=False, enable_pickup=False, + ); + + # storing all players for ScorinG. + self.joined_player_list.append(player); + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True; + + # very helpful function to check end game when player dead or leav. + def _check_end_game(self) -> bool: + + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + if living_team_count <= 0: + # kill the coutdown timer incase the all players dead before game is about to going to be end. + # so, countdown won't call the function. + # FIXE ME: it's that ok to kill this timer? + # self._countdown._timer = None; + self.end_game() + + # this function called when player leave. + def on_player_leave(self, player: Player) -> NoReturn: + # Augment default behavior. + super().on_player_leave(player); + + # checking end game. + self._check_end_game(); + + # this gamemode needs to handle only one msg "PlayerDiedMessage". + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg); + + # and storing the dead player records in our dead_player_list. + self.dead_player_list.append(msg.getplayer(Player)); + + # check the end game. + ba.timer(1.0, self._check_end_game); + + def end_game(self): + # kill timers + self.box.stop_shoot() + self.check_player_pos_timer = None + self.shield_drop_timer = None + + # here the player_dead_list and joined_player_list gonna be very helpful. + for team in self.teams: + for player in team.players: + + # for scoring i am just following the index of the player_dead_list. + # for dead list... + # 0th index player dead first. + # 1st index player dead second. + # and so on... + # i think you got it... maybe + # sometime we also got a empty list + # if we got a empty list that means all players are survived or maybe only one player playing and he/she survived. + if len(self.dead_player_list) > 0: + + for index, dead_player in enumerate(self.dead_player_list): + # if this condition is true we find the dead player \ + # and his index with enumerate function. + if player == dead_player: + # updating with one, because i don't want to give 0 score to first dead player. + index += 1 + break + # and if this statement is true we just find a survived player. + # for survived player i am giving the highest score according to how many players are joined. + elif index == len(self.dead_player_list) - 1: + index = len(self.joined_player_list) + # for survived player i am giving the highest score according to how many players are joined. + else: + index = len(self.joined_player_list) + + # and here i am following Table of 10 for scoring. + # very lazY. + score = int(10 * index) + + self.stats.player_scored(player, score, screenmessage=False) + + # Ok now calc game results: set a score for each team and then tell \ + # the game to end. + results = ba.GameResults() + + # Remember that 'free-for-all' mode is simply a special form \ + # of 'teams' mode where each player gets their own team, so we can \ + # just always deal in teams and have all cases covered. + # hmmm... some eric comments might be helpful to you. + for team in self.teams: + + max_index = 0 + for player in team.players: + # for the team, we choose only one player who survived longest. + # same logic.. + if len(self.dead_player_list) > 0: + for index, dead_player in enumerate(self.dead_player_list): + if player == dead_player: + index += 1 + break + elif index == len(self.dead_player_list) - 1: + index = len(self.joined_player_list) + else: + index = len(self.joined_player_list) + + max_index = max(max_index, index) + # set the team score + results.set_team_score(team, int(10 * max_index)) + # and end the game + self.end(results=results) + + \ No newline at end of file diff --git a/plugins/minigames/invisible_one.py b/plugins/minigames/invisible_one.py new file mode 100644 index 0000000..211a163 --- /dev/null +++ b/plugins/minigames/invisible_one.py @@ -0,0 +1,333 @@ +# Released under the MIT License. See LICENSE for details. +# +# By itsre3 +# =>3<= +# Don't mind my spelling. i realized that they were not correct after making last change and saving +# Besides that, enjoy.......!! +"""Provides the chosen-one mini-game.""" + +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.flag import Flag +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Optional, Sequence, Union + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.chosen_light: Optional[ba.NodeActor] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self, time_remaining: int) -> None: + self.time_remaining = time_remaining + + +# ba_meta export game +class InvicibleOneGame(ba.TeamGameActivity[Player, Team]): + """ + Game involving trying to remain the one 'invisible one' + for a set length of time while everyone else tries to + kill you and become the invisible one themselves. + """ + + name = 'Invisible One' + description = ('Be the invisible one for a length of time to win.\n' + 'Kill the invisible one to become it.') + available_settings = [ + ba.IntSetting( + 'Invicible One Time', + min_value=10, + default=30, + increment=10, + ), + ba.BoolSetting('Invicible one is lazy', default=True), + ba.BoolSetting('Night mode', default=False), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + scoreconfig = ba.ScoreConfig(label='Time Held') + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('keep_away') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._invicible_one_player: Optional[Player] = None + self._swipsound = ba.getsound('swip') + self._countdownsounds: Dict[int, ba.Sound] = { + 10: ba.getsound('announceTen'), + 9: ba.getsound('announceNine'), + 8: ba.getsound('announceEight'), + 7: ba.getsound('announceSeven'), + 6: ba.getsound('announceSix'), + 5: ba.getsound('announceFive'), + 4: ba.getsound('announceFour'), + 3: ba.getsound('announceThree'), + 2: ba.getsound('announceTwo'), + 1: ba.getsound('announceOne') + } + self._flag_spawn_pos: Optional[Sequence[float]] = None + self._reset_region_material: Optional[ba.Material] = None + self._flag: Optional[Flag] = None + self._reset_region: Optional[ba.Node] = None + self._epic_mode = bool(settings['Epic Mode']) + self._invicible_one_time = int(settings['Invicible One Time']) + self._time_limit = float(settings['Time Limit']) + self._invicible_one_is_lazy = bool(settings['Invicible one is lazy']) + self._night_mode = bool(settings['Night mode']) + + # Base class overrides + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.CHOSEN_ONE) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Show your invisibility powers.' + + def create_team(self, sessionteam: ba.SessionTeam) -> Team: + return Team(time_remaining=self._invicible_one_time) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + if self._get_invicible_one_player() is player: + self._set_invicible_one_player(None) + + def on_begin(self) -> None: + super().on_begin() + shared = SharedObjects.get() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._flag_spawn_pos = self.map.get_flag_position(None) + Flag.project_stand(self._flag_spawn_pos) + self._set_invicible_one_player(None) + if self._night_mode: + gnode = ba.getactivity().globalsnode + gnode.tint = (0.4, 0.4, 0.4) + + pos = self._flag_spawn_pos + ba.timer(1.0, call=self._tick, repeat=True) + + mat = self._reset_region_material = ba.Material() + mat.add_actions( + conditions=( + 'they_have_material', + shared.player_material, + ), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + ba.WeakCall(self._handle_reset_collide)), + ), + ) + + self._reset_region = ba.newnode('region', + attrs={ + 'position': (pos[0], pos[1] + 0.75, + pos[2]), + 'scale': (0.5, 0.5, 0.5), + 'type': 'sphere', + 'materials': [mat] + }) + + def _get_invicible_one_player(self) -> Optional[Player]: + # Should never return invalid references; return None in that case. + if self._invicible_one_player: + return self._invicible_one_player + return None + + def _handle_reset_collide(self) -> None: + # If we have a chosen one, ignore these. + if self._get_invicible_one_player() is not None: + return + + # Attempt to get a Player controlling a Spaz that we hit. + try: + player = ba.getcollision().opposingnode.getdelegate( + PlayerSpaz, True).getplayer(Player, True) + except ba.NotFoundError: + return + + if player.is_alive(): + self._set_invicible_one_player(player) + + def _flash_flag_spawn(self) -> None: + light = ba.newnode('light', + attrs={ + 'position': self._flag_spawn_pos, + 'color': (1, 1, 1), + 'radius': 0.3, + 'height_attenuated': False + }) + ba.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _tick(self) -> None: + + # Give the chosen one points. + player = self._get_invicible_one_player() + if player is not None: + + # This shouldn't happen, but just in case. + if not player.is_alive(): + ba.print_error('got dead player as chosen one in _tick') + self._set_invicible_one_player(None) + else: + scoring_team = player.team + assert self.stats + self.stats.player_scored(player, + 3, + screenmessage=False, + display=False) + + scoring_team.time_remaining = max( + 0, scoring_team.time_remaining - 1) + + + self._update_scoreboard() + + # announce numbers we have sounds for + if scoring_team.time_remaining in self._countdownsounds: + ba.playsound( + self._countdownsounds[scoring_team.time_remaining]) + + # Winner! + if scoring_team.time_remaining <= 0: + self.end_game() + + else: + # (player is None) + # This shouldn't happen, but just in case. + # (Chosen-one player ceasing to exist should + # trigger on_player_leave which resets chosen-one) + if self._invicible_one_player is not None: + ba.print_error('got nonexistent player as chosen one in _tick') + self._set_invicible_one_player(None) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, + self._invicible_one_time - team.time_remaining) + self.end(results=results, announce_delay=0) + + def _set_invicible_one_player(self, player: Optional[Player]) -> None: + existing = self._get_invicible_one_player() + if existing: + existing.chosen_light = None + ba.playsound(self._swipsound) + if not player: + assert self._flag_spawn_pos is not None + self._flag = Flag(color=(1, 0.9, 0.2), + position=self._flag_spawn_pos, + touchable=False) + self._invicible_one_player = None + + # Create a light to highlight the flag; + # this will go away when the flag dies. + ba.newnode('light', + owner=self._flag.node, + attrs={ + 'position': self._flag_spawn_pos, + 'intensity': 0.6, + 'height_attenuated': False, + 'volume_intensity_scale': 0.1, + 'radius': 0.1, + 'color': (1.2, 1.2, 0.4) + }) + + # Also an extra momentary flash. + self._flash_flag_spawn() + else: + if player.actor: + self._flag = None + self._invicible_one_player = player + + if self._invicible_one_is_lazy: + player.actor.connect_controls_to_player(enable_punch = False, enable_pickup = False, enable_bomb = False) + if player.actor.node.torso_model != None: + player.actor.node.color_mask_texture = None + player.actor.node.color_texture = None + player.actor.node.head_model = None + player.actor.node.torso_model = None + player.actor.node.upper_arm_model = None + player.actor.node.forearm_model = None + player.actor.node.pelvis_model = None + player.actor.node.toes_model = None + player.actor.node.upper_leg_model = None + player.actor.node.lower_leg_model = None + player.actor.node.hand_model = None + player.actor.node.style = 'cyborg' + invi_sound = [] + player.actor.node.jump_sounds = invi_sound + player.actor.attack_sounds = invi_sound + player.actor.impact_sounds = invi_sound + player.actor.pickup_sounds = invi_sound + player.actor.death_sounds = invi_sound + player.actor.fall_sounds = invi_sound + + player.actor.node.name = '' + + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + if player is self._get_invicible_one_player(): + killerplayer = msg.getkillerplayer(Player) + self._set_invicible_one_player(None if ( + killerplayer is None or killerplayer is player + or not killerplayer.is_alive()) else killerplayer) + self.respawn_player(player) + else: + super().handlemessage(msg) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, + team.time_remaining, + self._invicible_one_time, + countdown=True) diff --git a/plugins/minigames/last_punch_stand.py b/plugins/minigames/last_punch_stand.py new file mode 100644 index 0000000..878fea9 --- /dev/null +++ b/plugins/minigames/last_punch_stand.py @@ -0,0 +1,263 @@ +# ba_meta require api 7 +from typing import Sequence +import ba, _ba, random +from bastd.actor.spaz import Spaz +from bastd.actor.scoreboard import Scoreboard + +class Player(ba.Player['Team']): + """Our player type for this game.""" + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.score = 1 + +class ChooseingSpazHitMessage: + def __init__(self, hitter:Player) -> None: + self.hitter = hitter + +class ChooseingSpazDieMessage: + def __init__(self, killer:Player) -> None: + self.killer = killer + +class ChooseingSpaz(Spaz): + def __init__( + self, + pos:Sequence[float], + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + ): + super().__init__(color, highlight, "Spaz", None, True, True, False, False) + self.last_player_attacked_by = None + self.stand(pos) + self.loc = ba.newnode( + 'locator', + attrs={ + 'shape': 'circleOutline', + 'position': pos, + 'color': color, + 'opacity': 1, + 'draw_beauty': False, + 'additive': True, + }, + ) + self.node.connectattr("position", self.loc, "position") + ba.animate_array(self.loc, "size", 1, keys={0:[0.5,], 1:[2,], 1.5:[0.5]}, loop=True) + + def handlemessage(self, msg): + if isinstance(msg, ba.FreezeMessage): + return + + if isinstance(msg, ba.PowerupMessage): + if not(msg.poweruptype == "health"): + return + + super().handlemessage(msg) + + if isinstance(msg, ba.HitMessage): + self.handlemessage(ba.PowerupMessage("health")) + + player = msg.get_source_player(Player) + if self.is_alive(): + self.activity.handlemessage(ChooseingSpazHitMessage(player)) + self.last_player_attacked_by = player + + + elif isinstance(msg, ba.DieMessage): + player = self.last_player_attacked_by + + if msg.how.value != ba.DeathType.GENERIC.value: + self._dead = True + self.activity.handlemessage(ChooseingSpazDieMessage(player)) + + self.loc.delete() + + + + def stand(self, pos = (0,0,0), angle = 0): + self.handlemessage(ba.StandMessage(pos,angle)) + + def recolor(self, color, highlight = (1,1,1)): + self.node.color = color + self.node.highlight = highlight + self.loc.color = color + +class ChooseBilbord(ba.Actor): + def __init__(self, player:Player, delay = 0.1) -> None: + super().__init__() + + icon = player.get_icon() + self.scale = 100 + + self.node = ba.newnode( + 'image', + delegate=self, + attrs={ + "position":(60,-125), + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'tint2_color': icon['tint2_color'], + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': "topLeft" + }, + ) + + self.name_node = ba.newnode( + 'text', + owner=self.node, + attrs={ + 'position': (60,-185), + 'text': ba.Lstr(value=player.getname()), + 'color': ba.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'flatness': 1.0, + 'h_attach': 'left', + 'v_attach': 'top', + 'maxwidth':self.scale + }, + ) + + ba.animate_array(self.node, "scale", keys={0 + delay:[0,0], 0.05 + delay:[self.scale, self.scale]}, size=1) + ba.animate(self.name_node, "scale", {0 + delay:0, 0.07 + delay:1}) + + def handlemessage(self, msg): + super().handlemessage(msg) + if isinstance(msg, ba.DieMessage): + ba.animate_array(self.node, "scale", keys={0:self.node.scale, 0.05:[0,0]}, size=1) + ba.animate(self.name_node, "scale", {0:self.name_node.scale, 0.07:0}) + + def __delete(): + self.node.delete() + self.name_node.delete() + + ba.timer(0.2, __delete) + +# ba_meta export game +class LastPunchStand(ba.TeamGameActivity[Player, Team]): + name = "Last Punch Stand" + description = "Last one punchs the choosing spaz wins" + tips = [ + 'keep punching the choosing spaz to be last punched player at times up!', + 'you can not frezz the choosing spaz', + "evry time you punch the choosing spaz, you will get one point", + ] + + default_music = ba.MusicType.TO_THE_DEATH + + available_settings = [ + ba.FloatSetting("min time limit (in seconds)", 50.0, min_value=30.0), + ba.FloatSetting("max time limit (in seconds)", 160.0, 60), + + ] + + def __init__(self, settings: dict): + super().__init__(settings) + self._min_timelimit = settings["min time limit (in seconds)"] + self._max_timelimit = settings["max time limit (in seconds)"] + if (self._min_timelimit > self._max_timelimit): + self._max_timelimit = self._min_timelimit + + self._choosing_spaz_defcolor = (0.5,0.5,0.5) + self.choosing_spaz = None + self.choosed_player = None + self.times_uped = False + self.scoreboard = Scoreboard() + + def times_up(self): + self.times_uped = True + + for player in self.players: + if self.choosed_player and (player.team.id != self.choosed_player.team.id): + player.actor._cursed = True + player.actor.curse_explode() + + self.end_game() + + def __get_spaz_bot_spawn_point(self): + if len(self.map.tnt_points) > 0: + return self.map.tnt_points[random.randint(0, len(self.map.tnt_points)-1)] + else: + return (0, 6, 0) + + def spaw_bot(self): + "spawns a choosing bot" + + self.choosing_spaz = ChooseingSpaz(self.__get_spaz_bot_spawn_point()) + self.choose_bilbord = None + + def on_begin(self) -> None: + super().on_begin() + time_limit = random.randint(self._min_timelimit, self._max_timelimit) + self.spaw_bot() + ba.timer(time_limit, self.times_up) + + self.setup_standard_powerup_drops(False) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + if self.choosed_player and (team.id == self.choosed_player.team.id): team.score += 100 + results.set_team_score(team, team.score) + self.end(results=results) + + def change_choosed_player(self, hitter:Player): + if hitter: + self.choosing_spaz.recolor(hitter.color, hitter.highlight) + self.choosed_player = hitter + hitter.team.score += 1 + self.choose_bilbord = ChooseBilbord(hitter) + self.hide_score_board() + else: + self.choosing_spaz.recolor(self._choosing_spaz_defcolor) + self.choosed_player = None + self.choose_bilbord = None + self.show_score_board() + + def show_score_board(self): + self.scoreboard = Scoreboard() + for team in self.teams: + self.scoreboard.set_team_value(team, team.score) + + def hide_score_board(self): + self.scoreboard = None + + def _watch_dog_(self): + "checks if choosing spaz exists" + #choosing spaz wont respawn if death type if generic + #this becuse we dont want to keep respawn him when he dies because of losing referce + #but sometimes "choosing spaz" dies naturaly and his death type is generic! so it wont respawn back again + #thats why we have this function; to check if spaz exits in the case that he didnt respawned + + if self.choosing_spaz: + if self.choosing_spaz._dead: + self.spaw_bot() + else: + self.spaw_bot() + + def handlemessage(self, msg): + super().handlemessage(msg) + + if isinstance(msg, ChooseingSpazHitMessage): + hitter = msg.hitter + if self.choosing_spaz.node and hitter: + self.change_choosed_player(hitter) + + elif isinstance(msg, ChooseingSpazDieMessage): + self.spaw_bot() + self.change_choosed_player(None) + + elif isinstance(msg, ba.PlayerDiedMessage): + player = msg.getplayer(Player) + if not (self.has_ended() or self.times_uped): + self.respawn_player(player, 0) + + if self.choosed_player and (player.getname(True) == self.choosed_player.getname(True)): + self.change_choosed_player(None) + + self._watch_dog_() \ No newline at end of file diff --git a/plugins/minigames/quake.py b/plugins/minigames/quake.py new file mode 100644 index 0000000..58ef3e3 --- /dev/null +++ b/plugins/minigames/quake.py @@ -0,0 +1,642 @@ +"""Quake Game Activity""" +# ba_meta require api 7 +from __future__ import annotations + +from typing import TYPE_CHECKING + +import random +import enum +import ba, _ba + +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBox +from bastd.gameutils import SharedObjects + +#from rocket +from bastd.actor.bomb import Blast + +#from railgun +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.spaz import Spaz + + +if TYPE_CHECKING: + from typing import Optional, List, Any, Type, Union, Sequence + + +STORAGE_ATTR_NAME = f'_shared_{__name__}_factory' + + +#+++++++++++++++++++Rocket++++++++++++++++++++++++ +class RocketFactory: + """Quake Rocket factory""" + + def __init__(self) -> None: + self.ball_material = ba.Material() + + self.ball_material.add_actions( + conditions=((('we_are_younger_than', 5), 'or', + ('they_are_younger_than', 5)), 'and', + ('they_have_material', + SharedObjects.get().object_material)), + actions=('modify_node_collision', 'collide', False)) + + self.ball_material.add_actions( + conditions=('they_have_material', + SharedObjects.get().pickup_material), + actions=('modify_part_collision', 'use_node_collide', False)) + + self.ball_material.add_actions(actions=('modify_part_collision', + 'friction', 0)) + + self.ball_material.add_actions( + conditions=(('they_have_material', + SharedObjects.get().footing_material), 'or', + ('they_have_material', + SharedObjects.get().object_material)), + actions=('message', 'our_node', 'at_connect', ImpactMessage())) + + @classmethod + def get(cls): + """Get factory if exists else create new""" + activity = ba.getactivity() + if hasattr(activity, STORAGE_ATTR_NAME): + return getattr(activity, STORAGE_ATTR_NAME) + factory = cls() + setattr(activity, STORAGE_ATTR_NAME, factory) + return factory + + +class RocketLauncher: + """Very dangerous weapon""" + + def __init__(self): + self.last_shot: Optional[int, float] = 0 + + def give(self, spaz: Spaz) -> None: + """Give spaz a rocket launcher""" + spaz.punch_callback = self.shot + self.last_shot = ba.time() + + # FIXME + # noinspection PyUnresolvedReferences + def shot(self, spaz: Spaz) -> None: + """Release a rocket""" + time = ba.time() + if time - self.last_shot > 0.6: + self.last_shot = time + center = spaz.node.position_center + forward = spaz.node.position_forward + direction = [center[0] - forward[0], forward[1] - center[1], + center[2] - forward[2]] + direction[1] = 0.0 + + mag = 10.0 / ba.Vec3(*direction).length() + vel = [v * mag for v in direction] + Rocket(position=spaz.node.position, + velocity=vel, + owner=spaz.getplayer(ba.Player), + source_player=spaz.getplayer(ba.Player), + color=spaz.node.color).autoretain() + + +class ImpactMessage: + """Rocket touched something""" + + +class Rocket(ba.Actor): + """Epic rocket from rocket launcher""" + + def __init__(self, + position=(0, 5, 0), + velocity=(1, 0, 0), + source_player=None, + owner=None, + color=(1.0, 0.2, 0.2)) -> None: + super().__init__() + self.source_player = source_player + self.owner = owner + self._color = color + factory = RocketFactory.get() + + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'model': ba.getmodel('impactBomb'), + 'body': 'sphere', + 'color_texture': ba.gettexture( + 'bunnyColor'), + 'model_scale': 0.2, + 'is_area_of_interest': True, + 'body_scale': 0.8, + 'materials': [ + SharedObjects.get().object_material, + factory.ball_material] + }) # yapf: disable + self.node.extra_acceleration = (self.node.velocity[0] * 200, 0, + self.node.velocity[2] * 200) + + self._life_timer = ba.Timer( + 5, ba.WeakCall(self.handlemessage, ba.DieMessage())) + + self._emit_timer = ba.Timer(0.001, ba.WeakCall(self.emit), repeat=True) + self.base_pos_y = self.node.position[1] + + ba.camerashake(5.0) + + def emit(self) -> None: + """Emit a trace after rocket""" + ba.emitfx(position=self.node.position, + scale=0.4, + spread=0.01, + chunk_type='spark') + if not self.node: + return + self.node.position = (self.node.position[0], self.base_pos_y, + self.node.position[2]) # ignore y + ba.newnode('explosion', + owner=self.node, + attrs={ + 'position': self.node.position, + 'radius': 0.2, + 'color': self._color + }) + + def handlemessage(self, msg: Any) -> Any: + """Message handling for rocket""" + super().handlemessage(msg) + if isinstance(msg, ImpactMessage): + self.node.handlemessage(ba.DieMessage()) + + elif isinstance(msg, ba.DieMessage): + if self.node: + Blast(position=self.node.position, + blast_radius=2, + source_player=self.source_player) + + self.node.delete() + self._emit_timer = None + + elif isinstance(msg, ba.OutOfBoundsMessage): + self.handlemessage(ba.DieMessage()) + +#-------------------Rocket-------------------------- + + +#++++++++++++++++++Railgun++++++++++++++++++++++++++ +class Railgun: + """Very dangerous weapon""" + + def __init__(self) -> None: + self.last_shot: Optional[int, float] = 0 + + def give(self, spaz: Spaz) -> None: + """Give spaz a railgun""" + spaz.punch_callback = self.shot + self.last_shot = ba.time() + + # FIXME + # noinspection PyUnresolvedReferences + def shot(self, spaz: Spaz) -> None: + """Release a rocket""" + time = ba.time() + if time - self.last_shot > 0.6: + self.last_shot = time + center = spaz.node.position_center + forward = spaz.node.position_forward + direction = [ + center[0] - forward[0], forward[1] - center[1], + center[2] - forward[2] + ] + direction[1] = 0.0 + + RailBullet(position=spaz.node.position, + direction=direction, + owner=spaz.getplayer(ba.Player), + source_player=spaz.getplayer(ba.Player), + color=spaz.node.color).autoretain() + + +class TouchedToSpazMessage: + """I hit!""" + + def __init__(self, spaz) -> None: + self.spaz = spaz + + +class RailBullet(ba.Actor): + """Railgun bullet""" + + def __init__(self, + position=(0, 5, 0), + direction=(0, 2, 0), + source_player=None, + owner=None, + color=(1, 1, 1)) -> None: + super().__init__() + self._color = color + + self.node = ba.newnode('light', + delegate=self, + attrs={ + 'position': position, + 'color': self._color + }) + ba.animate(self.node, 'radius', {0: 0, 0.1: 0.5, 0.5: 0}) + + self.source_player = source_player + self.owner = owner + self._life_timer = ba.Timer( + 0.5, ba.WeakCall(self.handlemessage, ba.DieMessage())) + + pos = position + vel = tuple(i / 5 for i in ba.Vec3(direction).normalized()) + for _ in range(500): # Optimization :( + ba.newnode('explosion', + owner=self.node, + attrs={ + 'position': pos, + 'radius': 0.2, + 'color': self._color + }) + pos = (pos[0] + vel[0], pos[1] + vel[1], pos[2] + vel[2]) + + for node in _ba.getnodes(): + if node and node.getnodetype() == 'spaz': + # pylint: disable=invalid-name + m3 = ba.Vec3(position) + a = ba.Vec3(direction[2], direction[1], direction[0]) + m1 = ba.Vec3(node.position) + # pylint: enable=invalid-name + # distance between node and line + dist = (a * (m1 - m3)).length() / a.length() + if dist < 0.3: + if node and node != self.owner and node.getdelegate( + PlayerSpaz, True).getplayer( + ba.Player, True).team != self.owner.team: + node.handlemessage(ba.FreezeMessage()) + pos = self.node.position + hit_dir = (0, 10, 0) + + node.handlemessage( + ba.HitMessage(pos=pos, + magnitude=50, + velocity_magnitude=50, + radius=0, + srcnode=self.node, + source_player=self.source_player, + force_direction=hit_dir)) + + def handlemessage(self, msg: Any) -> Any: + super().handlemessage(msg) + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, ba.OutOfBoundsMessage): + self.handlemessage(ba.DieMessage()) + +#------------------Railgun------------------------- + +class Player(ba.Player['Team']): + """Our player""" + + +class Team(ba.Team[Player]): + """Our team""" + def __init__(self) -> None: + self.score = 0 + + +class WeaponType(enum.Enum): + """Type of weapon""" + ROCKET = 0 + RAILGUN = 1 + + +class ObstaclesForm(enum.Enum): + """Obstacle form""" + CUBE = 0 + SPHERE = 1 + RANDOM = 2 + + +# ba_meta export game +class QuakeGame(ba.TeamGameActivity[Player, Team]): + """Quake Team Game Activity""" + name = 'Quake' + description = 'Kill a set number of enemies to win.' + available_settings = [ + ba.IntSetting( + 'Kills to Win Per Player', + default=15, + min_value=1, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[('None', 0), ('1 Minute', 60), ('2 Minutes', 120), + ('5 Minutes', 300), ('10 Minutes', 600), + ('20 Minutes', 1200)], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[('At once', 0.0), ('Shorter', 0.25), ('Short', 0.5), + ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], + default=1.0, + ), + ba.BoolSetting( + 'Speed', + default=True, + ), + ba.BoolSetting( + 'Enable Jump', + default=True, + ), + ba.BoolSetting( + 'Enable Pickup', + default=True, + ), + ba.BoolSetting( + 'Enable Bomb', + default=False, + ), + ba.BoolSetting( + 'Obstacles', + default=True, + ), + ba.IntChoiceSetting( + 'Obstacles Form', + choices=[('Cube', ObstaclesForm.CUBE.value), + ('Sphere', ObstaclesForm.SPHERE.value), + ('Random', ObstaclesForm.RANDOM.value)], + default=0, + ), + ba.IntChoiceSetting( + 'Weapon Type', + choices=[('Rocket', WeaponType.ROCKET.value), + ('Railgun', WeaponType.RAILGUN.value)], + default=WeaponType.ROCKET.value, + ), + ba.BoolSetting( + 'Obstacles Mirror Shots', + default=False, + ), + ba.IntSetting( + 'Obstacles Count', + default=16, + min_value=0, + increment=2, + ), + ba.BoolSetting( + 'Random Obstacles Color', + default=True, + ), + ba.BoolSetting( + 'Epic Mode', + default=False, + ), + ] + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.MultiTeamSession) or issubclass( + sessiontype, ba.FreeForAllSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + # TODO add more maps + return ['Football Stadium', 'Monkey Face', 'Doom Shroom'] + + def __init__(self, settings) -> None: + super().__init__(settings) + self._epic_mode = self.settings_raw['Epic Mode'] + self._score_to_win = self.settings_raw['Kills to Win Per Player'] + self._time_limit = self.settings_raw['Time Limit'] + self._obstacles_enabled = self.settings_raw['Obstacles'] + self._obstacles_count = self.settings_raw['Obstacles Count'] + self._speed_enabled = self.settings_raw['Speed'] + self._bomb_enabled = self.settings_raw['Enable Bomb'] + self._pickup_enabled = self.settings_raw['Enable Pickup'] + self._jump_enabled = self.settings_raw['Enable Jump'] + self._weapon_type = WeaponType(self.settings_raw['Weapon Type']) + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.GRAND_ROMP) + self.slow_motion = self._epic_mode + + self.announce_player_deaths = True + self._scoreboard = Scoreboard() + self._ding_sound = ba.getsound('dingSmall') + + self._shield_dropper: Optional[ba.Timer] = None + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Kill ${ARG1} enemies.', self._score_to_win + + def on_team_join(self, team: Team) -> None: + team.score = 0 + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + ba.TeamGameActivity.on_begin(self) + ba.getactivity().globalsnode.tint = (0.5, 0.7, 1) + self.drop_shield() + self._shield_dropper = ba.Timer(8, + ba.WeakCall(self.drop_shield), + repeat=True) + self.setup_standard_time_limit(self._time_limit) + if self._obstacles_enabled: + count = self._obstacles_count + gamemap = self.map.getname() + for i in range(count): # TODO: tidy up around here + if gamemap == 'Football Stadium': + radius = (random.uniform(-10, 1), + 6, + random.uniform(-4.5, 4.5)) \ + if i > count / 2 else ( + random.uniform(10, 1), 6, random.uniform(-4.5, 4.5)) + else: + radius = (random.uniform(-10, 1), + 6, + random.uniform(-8, 8)) \ + if i > count / 2 else ( + random.uniform(10, 1), 6, random.uniform(-8, 8)) + + Obstacle( + position=radius, + mirror=self.settings_raw['Obstacles Mirror Shots'], + form=self.settings_raw['Obstacles Form']).autoretain() + + self._update_scoreboard() + + def drop_shield(self) -> None: + """Drop a shield powerup in random place""" + # FIXME: should use map defs + shield = PowerupBox(poweruptype='shield', + position=(random.uniform(-10, 10), 6, + random.uniform(-5, 5))).autoretain() + + ba.playsound(self._ding_sound) + + p_light = ba.newnode('light', + owner=shield.node, + attrs={ + 'position': (0, 0, 0), + 'color': (0.3, 0.0, 0.4), + 'radius': 0.3, + 'intensity': 2, + 'volume_intensity_scale': 10.0 + }) + + shield.node.connectattr('position', p_light, 'position') + + ba.animate(p_light, 'intensity', {0: 2, 8: 0}) + + def spawn_player(self, player: Player) -> None: + spaz = self.spawn_player_spaz(player) + if self._weapon_type == WeaponType.ROCKET: + RocketLauncher().give(spaz) + elif self._weapon_type == WeaponType.RAILGUN: + Railgun().give(spaz) + spaz.connect_controls_to_player(enable_jump=self._jump_enabled, + enable_pickup=self._pickup_enabled, + enable_bomb=self._bomb_enabled, + enable_fly=False) + + spaz.node.hockey = self._speed_enabled + spaz.spaz_light = ba.newnode('light', + owner=spaz.node, + attrs={ + 'position': (0, 0, 0), + 'color': spaz.node.color, + 'radius': 0.12, + 'intensity': 1, + 'volume_intensity_scale': 10.0 + }) + + spaz.node.connectattr('position', spaz.spaz_light, 'position') + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + ba.TeamGameActivity.handlemessage(self, msg) + player = msg.getplayer(Player) + self.respawn_player(player) + killer = msg.getkillerplayer(Player) + if killer is None: + return + + # handle team-kills + if killer.team is player.team: + # in free-for-all, killing yourself loses you a point + if isinstance(self.session, ba.FreeForAllSession): + new_score = player.team.score - 1 + new_score = max(0, new_score) + player.team.score = new_score + # in teams-mode it gives a point to the other team + else: + ba.playsound(self._ding_sound) + for team in self.teams: + if team is not killer.team: + team.score += 1 + # killing someone on another team nets a kill + else: + killer.team.score += 1 + ba.playsound(self._ding_sound) + # in FFA show our score since its hard to find on + # the scoreboard + assert killer.actor is not None + # noinspection PyUnresolvedReferences + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # if someone has won, set a timer to end shortly + # (allows the dust to clear and draws to occur if + # deaths are close enough) + if any(team.score >= self._score_to_win for team in self.teams): + ba.timer(0.5, self.end_game) + + else: + ba.TeamGameActivity.handlemessage(self, msg) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + + self.end(results=results) + + +class Obstacle(ba.Actor): + """Scene object""" + + def __init__(self, + position, + form=ObstaclesForm.CUBE, + mirror=False) -> None: + ba.Actor.__init__(self) + + if form == ObstaclesForm.CUBE: + model = 'tnt' + body = 'crate' + elif form == ObstaclesForm.SPHERE: + model = 'bomb' + body = 'sphere' + else: # ObstaclesForm.RANDOM: + model = random.choice(['tnt', 'bomb']) + body = 'sphere' if model == 'bomb' else 'crate' + + self.node = ba.newnode( + 'prop', + delegate=self, + attrs={ + 'position': + position, + 'model': + ba.getmodel(model), + 'body': + body, + 'body_scale': + 1.3, + 'model_scale': + 1.3, + 'reflection': + 'powerup', + 'reflection_scale': [0.7], + 'color_texture': + ba.gettexture('bunnyColor'), + 'materials': [SharedObjects.get().footing_material] + if mirror else [ + SharedObjects.get().object_material, + SharedObjects.get().footing_material + ] + }) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, ba.OutOfBoundsMessage): + if self.node: + self.handlemessage(ba.DieMessage()) + + elif isinstance(msg, ba.HitMessage): + self.node.handlemessage('impulse', msg.pos[0], msg.pos[1], + msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], + msg.magnitude, msg.velocity_magnitude, + msg.radius, 0, msg.velocity[0], + msg.velocity[1], msg.velocity[2]) diff --git a/plugins/minigames/sleep_race.py b/plugins/minigames/sleep_race.py new file mode 100644 index 0000000..6746524 --- /dev/null +++ b/plugins/minigames/sleep_race.py @@ -0,0 +1,783 @@ +# Released under the MIT License. See LICENSE for details. +# y me (: itsre3 +# =>2<= +#BCS RULES +# +"""Defines Race mini-game.""" + +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +from dataclasses import dataclass + +import ba, _ba +from bastd.actor.bomb import Bomb +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict, + Union) + from bastd.actor.onscreentimer import OnScreenTimer + + +@dataclass +class RaceMine: + """Holds info about a mine on the track.""" + point: Sequence[float] + mine: Optional[Bomb] + + +class RaceRegion(ba.Actor): + """Region used to track progress during a race.""" + + def __init__(self, pt: Sequence[float], index: int): + super().__init__() + activity = self.activity + assert isinstance(activity, RaceGame) + self.pos = pt + self.index = index + self.node = ba.newnode( + 'region', + delegate=self, + attrs={ + 'position': pt[:3], + 'scale': (pt[3] * 2.0, pt[4] * 2.0, pt[5] * 2.0), + 'type': 'box', + 'materials': [activity.race_region_material] + }) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.distance_txt: Optional[ba.Node] = None + self.last_region = 0 + self.lap = 0 + self.distance = 0.0 + self.finished = False + self.rank: Optional[int] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.time: Optional[float] = None + self.lap = 0 + self.finished = False + + +# ba_meta export game +class SleepRaceGame(ba.TeamGameActivity[Player, Team]): + """Game of racing around a track.""" + + name = 'Sleep Race' + description = 'Can you run while sleeping?' + scoreconfig = ba.ScoreConfig(label='Time', + lower_is_better=True, + scoretype=ba.ScoreType.MILLISECONDS) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting('Laps', min_value=1, default=2, increment=1), + ba.IntChoiceSetting( + 'Time Limit', + default=0, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + ), + ba.IntChoiceSetting( + 'Mine Spawning', + default=4000, + choices=[ + ('No Mines', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ], + ), + ba.IntChoiceSetting( + 'Bomb Spawning', + choices=[ + ('None', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ('1 Second', 1000), + ], + default=2000, + ), + ba.IntChoiceSetting( + 'Knockout Time', + choices=[ + ('8 Seconds', 8000), + ('5 Seconds', 5000), + ], + default=5000, + ), + ba.BoolSetting('Epic Mode', default=False), + ba.BoolSetting('Credits', default=True), + ] + + # We have some specific settings in teams mode. + if issubclass(sessiontype, ba.DualTeamSession): + settings.append( + ba.BoolSetting('Entire Team Must Finish', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.MultiTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('race') + + def __init__(self, settings: dict): + self._race_started = False + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_sound = ba.getsound('score') + self._swipsound = ba.getsound('swip') + self._last_team_time: Optional[float] = None + self._front_race_region: Optional[int] = None + self._nub_tex = ba.gettexture('nub') + self._beep_1_sound = ba.getsound('raceBeep1') + self._beep_2_sound = ba.getsound('raceBeep2') + self.race_region_material: Optional[ba.Material] = None + self._regions: List[RaceRegion] = [] + self._team_finish_pts: Optional[int] = None + self._time_text: Optional[ba.Actor] = None + self._cd_text: Optional[ba.Actor] = None + self._timer: Optional[OnScreenTimer] = None + self._race_mines: Optional[List[RaceMine]] = None + self._race_mine_timer: Optional[ba.Timer] = None + self._scoreboard_timer: Optional[ba.Timer] = None + self._player_order_update_timer: Optional[ba.Timer] = None + self._start_lights: Optional[List[ba.Node]] = None + self._bomb_spawn_timer: Optional[ba.Timer] = None + self._knockout_timer: Optional[ba.Timer] = None + self._laps = int(settings['Laps']) + self._entire_team_must_finish = bool( + settings.get('Entire Team Must Finish', False)) + self._time_limit = float(settings['Time Limit']) + self._mine_spawning = int(settings['Mine Spawning']) + self._bomb_spawning = int(settings['Bomb Spawning']) + self._knockout_time = float(settings['Knockout Time']) + self._epic_mode = bool(settings['Epic Mode']) + self._credits = bool(settings['Credits']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC_RACE + if self._epic_mode else ba.MusicType.RACE) + + def get_instance_description(self) -> Union[str, Sequence]: + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + t_str = ' Your entire team has to finish.' + else: + t_str = '' + + if self._laps > 1: + return 'Run ${ARG1} laps.' + t_str, self._laps + return 'Run 1 lap.' + t_str + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._laps > 1: + return 'run ${ARG1} laps', self._laps + return 'run 1 lap' + + def on_transition_in(self) -> None: + super().on_transition_in() + shared = SharedObjects.get() + pts = self.map.get_def_points('race_point') + mat = self.race_region_material = ba.Material() + mat.add_actions(conditions=('they_have_material', + shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + self._handle_race_point_collide), + )) + for rpt in pts: + self._regions.append(RaceRegion(rpt, len(self._regions))) + + def _flash_player(self, player: Player, scale: float) -> None: + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + pos = player.actor.node.position + light = ba.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + ba.timer(0.5, light.delete) + ba.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) + + def _handle_race_point_collide(self) -> None: + # FIXME: Tidy this up. + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + collision = ba.getcollision() + try: + region = collision.sourcenode.getdelegate(RaceRegion, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + last_region = player.last_region + this_region = region.index + + if last_region != this_region: + + # If a player tries to skip regions, smite them. + # Allow a one region leeway though (its plausible players can get + # blown over a region, etc). + if this_region > last_region + 2: + if player.is_alive(): + assert player.actor + player.actor.handlemessage(ba.DieMessage()) + ba.screenmessage(ba.Lstr( + translate=('statements', 'Killing ${NAME} for' + ' skipping part of the track!'), + subs=[('${NAME}', player.getname(full=True))]), + color=(1, 0, 0)) + else: + # If this player is in first, note that this is the + # front-most race-point. + if player.rank == 0: + self._front_race_region = this_region + + player.last_region = this_region + if last_region >= len(self._regions) - 2 and this_region == 0: + team = player.team + player.lap = min(self._laps, player.lap + 1) + + # In teams mode with all-must-finish on, the team lap + # value is the min of all team players. + # Otherwise its the max. + if isinstance(self.session, ba.DualTeamSession + ) and self._entire_team_must_finish: + team.lap = min([p.lap for p in team.players]) + else: + team.lap = max([p.lap for p in team.players]) + + # A player is finishing. + if player.lap == self._laps: + + # In teams mode, hand out points based on the order + # players come in. + if isinstance(self.session, ba.DualTeamSession): + assert self._team_finish_pts is not None + if self._team_finish_pts > 0: + self.stats.player_scored(player, + self._team_finish_pts, + screenmessage=False) + self._team_finish_pts -= 25 + + # Flash where the player is. + self._flash_player(player, 1.0) + player.finished = True + assert player.actor + player.actor.handlemessage( + ba.DieMessage(immediate=True)) + + # Makes sure noone behind them passes them in rank + # while finishing. + player.distance = 9999.0 + + # If the whole team has finished the race. + if team.lap == self._laps: + ba.playsound(self._score_sound) + player.team.finished = True + assert self._timer is not None + elapsed = ba.time() - self._timer.getstarttime() + self._last_team_time = player.team.time = elapsed + self._check_end_game() + + # Team has yet to finish. + else: + ba.playsound(self._swipsound) + + # They've just finished a lap but not the race. + else: + ba.playsound(self._swipsound) + self._flash_player(player, 0.3) + + # Print their lap number over their head. + try: + assert isinstance(player.actor, PlayerSpaz) + mathnode = ba.newnode('math', + owner=player.actor.node, + attrs={ + 'input1': (0, 1.9, 0), + 'operation': 'add' + }) + player.actor.node.connectattr( + 'torso_position', mathnode, 'input2') + tstr = ba.Lstr(resource='lapNumberText', + subs=[('${CURRENT}', + str(player.lap + 1)), + ('${TOTAL}', str(self._laps)) + ]) + txtnode = ba.newnode('text', + owner=mathnode, + attrs={ + 'text': tstr, + 'in_world': True, + 'color': (1, 1, 0, 1), + 'scale': 0.015, + 'h_align': 'center' + }) + mathnode.connectattr('output', txtnode, 'position') + ba.animate(txtnode, 'scale', { + 0.0: 0, + 0.2: 0.019, + 2.0: 0.019, + 2.2: 0 + }) + ba.timer(2.3, mathnode.delete) + except Exception: + ba.print_exception('Error printing lap.') + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + + # A player leaving disqualifies the team if 'Entire Team Must Finish' + # is on (otherwise in teams mode everyone could just leave except the + # leading player to win). + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + ba.screenmessage(ba.Lstr( + translate=('statements', + '${TEAM} is disqualified because ${PLAYER} left'), + subs=[('${TEAM}', player.team.name), + ('${PLAYER}', player.getname(full=True))]), + color=(1, 1, 0)) + player.team.finished = True + player.team.time = None + player.team.lap = 0 + ba.playsound(ba.getsound('boo')) + for otherplayer in player.team.players: + otherplayer.lap = 0 + otherplayer.finished = True + try: + if otherplayer.actor is not None: + otherplayer.actor.handlemessage(ba.DieMessage()) + except Exception: + ba.print_exception('Error sending DieMessage.') + + # Defer so team/player lists will be updated. + ba.pushcall(self._check_end_game) + + def _update_scoreboard(self) -> None: + for team in self.teams: + distances = [player.distance for player in team.players] + if not distances: + teams_dist = 0.0 + else: + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + teams_dist = min(distances) + else: + teams_dist = max(distances) + self._scoreboard.set_team_value( + team, + teams_dist, + self._laps, + flash=(teams_dist >= float(self._laps)), + show_value=False) + + def on_begin(self) -> None: + from bastd.actor.onscreentimer import OnScreenTimer + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._team_finish_pts = 100 + if self._credits: + self._cd_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'position': (0, 0), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (1, 1, 1), + 'text': 'By itsre3' + })) + + + # Throw a timer up on-screen. + self._time_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.5, 1), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, -50), + 'scale': 1.4, + 'text': '' + })) + self._timer = OnScreenTimer() + + if self._mine_spawning != 0: + self._race_mines = [ + RaceMine(point=p, mine=None) + for p in self.map.get_def_points('race_mine') + ] + if self._race_mines: + self._race_mine_timer = ba.Timer(0.001 * self._mine_spawning, + self._update_race_mine, + repeat=True) + + self._scoreboard_timer = ba.Timer(0.25, + self._update_scoreboard, + repeat=True) + self._player_order_update_timer = ba.Timer(0.25, + self._update_player_order, + repeat=True) + + if self.slow_motion: + t_scale = 0.4 + light_y = 50 + else: + t_scale = 1.0 + light_y = 150 + lstart = 7.1 * t_scale + inc = 1.25 * t_scale + + ba.timer(lstart, self._do_light_1) + ba.timer(lstart + inc, self._do_light_2) + ba.timer(lstart + 2 * inc, self._do_light_3) + ba.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = ba.newnode('image', + attrs={ + 'texture': ba.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + ba.animate( + lnub, 'opacity', { + 4.0 * t_scale: 0, + 5.0 * t_scale: 1.0, + 12.0 * t_scale: 1.0, + 12.5 * t_scale: 0.0 + }) + ba.timer(13.0 * t_scale, lnub.delete) + self._start_lights.append(lnub) + + self._start_lights[0].color = (0.2, 0, 0) + self._start_lights[1].color = (0.2, 0, 0) + self._start_lights[2].color = (0.2, 0.05, 0) + self._start_lights[3].color = (0.0, 0.3, 0) + + def _do_light_1(self) -> None: + assert self._start_lights is not None + self._start_lights[0].color = (1.0, 0, 0) + ba.playsound(self._beep_1_sound) + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + ba.playsound(self._beep_1_sound) + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + ba.playsound(self._beep_1_sound) + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + ba.playsound(self._beep_2_sound) + for player in self.players: + if player.actor is not None: + try: + assert isinstance(player.actor, PlayerSpaz) + player.actor.connect_controls_to_player() + except Exception: + ba.print_exception('Error in race player connects.') + assert self._timer is not None + self._timer.start() + + if self._bomb_spawning != 0: + self._bomb_spawn_timer = ba.Timer(0.001 * self._bomb_spawning, + self._spawn_bomb, + repeat=True) + + + def knock_players(): + activity = _ba.get_foreground_host_activity() + gnode = ba.getactivity().globalsnode + for players in activity.players: + gnode.tint = (0.5,0.5,0.5) + node = players.actor.node + node.handlemessage('knockout', 600.0) + self.text_offset = ba.newnode('math', + owner=node, + attrs={'input1': (-0.5, 0.5, 0.25), + 'operation': 'add'}) + node.connectattr( + 'torso_position', + self.text_offset, + 'input2') + self.text = ba.newnode('text', + owner=node, + attrs={ + 'h_align': 'right', + 'color': (1.0, 1.0, 1.0), + 'shadow': 1.0, + 'text': 'z z', + 'scale': 0.01, + 'in_world': True}) + self.text_offset.connectattr( + 'output', + self.text, + 'position') + ba.animate(self.text, 'scale', {0: 0.0, 1.0: 0.01}) + ba.timer(2, self.text.delete) + + if self._knockout_time != 0: + knock_time = 0.001 * self._knockout_time + self._knockout_timer = ba.Timer(knock_time, + knock_players, + repeat=True) + + self._race_started = True + + + def _update_player_order(self) -> None: + + # Calc all player distances. + for player in self.players: + pos: Optional[ba.Vec3] + try: + pos = player.position + except ba.NotFoundError: + pos = None + if pos is not None: + r_index = player.last_region + rg1 = self._regions[r_index] + r1pt = ba.Vec3(rg1.pos[:3]) + rg2 = self._regions[0] if r_index == len( + self._regions) - 1 else self._regions[r_index + 1] + r2pt = ba.Vec3(rg2.pos[:3]) + r2dist = (pos - r2pt).length() + amt = 1.0 - (r2dist / (r2pt - r1pt).length()) + amt = player.lap + (r_index + amt) * (1.0 / len(self._regions)) + player.distance = amt + + # Sort players by distance and update their ranks. + p_list = [(player.distance, player) for player in self.players] + + p_list.sort(reverse=True, key=lambda x: x[0]) + for i, plr in enumerate(p_list): + plr[1].rank = i + if plr[1].actor: + node = plr[1].distance_txt + if node: + node.text = str(i + 1) if plr[1].is_alive() else '' + + def _spawn_bomb(self) -> None: + if self._front_race_region is None: + return + region = (self._front_race_region + 3) % len(self._regions) + pos = self._regions[region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1] + 1.0, + pos[2] + random.uniform(*z_range)) + ba.timer(random.uniform(0.0, 2.0), + ba.WeakCall(self._spawn_bomb_at_pos, pos)) + + def _spawn_bomb_at_pos(self, pos: Sequence[float]) -> None: + if self.has_ended(): + return + Bomb(position=pos, bomb_type='normal').autoretain() + + def _make_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + rmine.mine = Bomb(position=rmine.point[:3], bomb_type='land_mine') + rmine.mine.arm() + + def _flash_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + light = ba.newnode('light', + attrs={ + 'position': rmine.point[:3], + 'color': (1, 0.2, 0.2), + 'radius': 0.1, + 'height_attenuated': False + }) + ba.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _update_race_mine(self) -> None: + assert self._race_mines is not None + m_index = -1 + rmine = None + for _i in range(3): + m_index = random.randrange(len(self._race_mines)) + rmine = self._race_mines[m_index] + if not rmine.mine: + break + assert rmine is not None + if not rmine.mine: + self._flash_mine(m_index) + ba.timer(0.95, ba.Call(self._make_mine, m_index)) + + def spawn_player(self, player: Player) -> ba.Actor: + if player.team.finished: + # FIXME: This is not type-safe! + # This call is expected to always return an Actor! + # Perhaps we need something like can_spawn_player()... + # noinspection PyTypeChecker + return None # type: ignore + pos = self._regions[player.last_region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1], + pos[2] + random.uniform(*z_range)) + spaz = self.spawn_player_spaz( + player, position=pos, angle=90 if not self._race_started else None) + assert spaz.node + # Prevent controlling of characters before the start of the race. + if not self._race_started: + spaz.disconnect_controls_from_player() + mathnode = ba.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + spaz.node.connectattr('torso_position', mathnode, 'input2') + + distance_txt = ba.newnode('text', + owner=spaz.node, + attrs={ + 'text': '', + 'in_world': True, + 'color': (1, 1, 0.4), + 'scale': 0.02, + 'h_align': 'center' + }) + player.distance_txt = distance_txt + mathnode.connectattr('output', distance_txt, 'position') + + def _check_end_game(self) -> None: + + # If there's no teams left racing, finish. + teams_still_in = len([t for t in self.teams if not t.finished]) + if teams_still_in == 0: + self.end_game() + return + + # Count the number of teams that have completed the race. + teams_completed = len( + [t for t in self.teams if t.finished and t.time is not None]) + + if teams_completed > 0: + session = self.session + + # In teams mode its over as soon as any team finishes the race + + # FIXME: The get_ffa_point_awards code looks dangerous. + if isinstance(session, ba.DualTeamSession): + self.end_game() + else: + # In ffa we keep the race going while there's still any points + # to be handed out. Find out how many points we have to award + # and how many teams have finished, and once that matches + # we're done. + assert isinstance(session, ba.FreeForAllSession) + points_to_award = len(session.get_ffa_point_awards()) + if teams_completed >= points_to_award - teams_completed: + self.end_game() + return + + def end_game(self) -> None: + + # Stop updating our time text, and set it to show the exact last + # finish time if we have one. (so users don't get upset if their + # final time differs from what they see onscreen by a tiny amount) + assert self._timer is not None + if self._timer.has_started(): + self._timer.stop( + endtime=None if self._last_team_time is None else ( + self._timer.getstarttime() + self._last_team_time)) + + results = ba.GameResults() + + for team in self.teams: + if team.time is not None: + # We store time in seconds, but pass a score in milliseconds. + results.set_team_score(team, int(team.time * 1000.0)) + else: + results.set_team_score(team, None) + + # We don't announce a winner in ffa mode since its probably been a + # while since the first place guy crossed the finish line so it seems + # odd to be announcing that now. + self.end(results=results, + announce_winning_team=isinstance(self.session, + ba.DualTeamSession)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # Augment default behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + if not player.finished: + self.respawn_player(player, respawn_time=1) + else: + super().handlemessage(msg) diff --git a/plugins/minigames/snake.py b/plugins/minigames/snake.py new file mode 100644 index 0000000..7252bfc --- /dev/null +++ b/plugins/minigames/snake.py @@ -0,0 +1,314 @@ +#snake +# Released under the MIT License. See LICENSE for details. +# +"""Snake game by SEBASTIAN2059""" + +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor import bomb as stdbomb + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + +class ScoreMessage: + """It will help us with the scores.""" + def __init__(self,player: Player): + self.player = player + + def getplayer(self): + return self.player + +class Player(ba.Player['Team']): + """Our player type for this game.""" + def __init__(self) -> None: + + self.mines = [] + self.actived = None + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + +lang = ba.app.lang.language +if lang == 'Spanish': + description = 'Sobrevive a un número determinado de minas para ganar.' + join_description = 'Corre y no te dejes matar.' + view_description = 'sobrevive ${ARG1} minas' + +else: + description = 'Survive a set number of mines to win.' + join_description = "Run and don't get killed." + view_description = 'survive ${ARG1} mines' + +class Custom_Mine(stdbomb.Bomb): + """Custom a mine :)""" + def __init__(self,position,source_player): + stdbomb.Bomb.__init__(self,position=position,bomb_type='land_mine',source_player=source_player) + + def handlemessage(self,msg: Any) -> Any: + if isinstance(msg, ba.HitMessage): + return + else: + super().handlemessage(msg) + +# ba_meta export game +class SnakeGame(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Snake' + description = description + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting( + 'Score to Win', + min_value=40, + default=80, + increment=5, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = ba.getsound('dingSmall') + + self._beep_1_sound = ba.getsound('raceBeep1') + self._beep_2_sound = ba.getsound('raceBeep2') + + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + self._started = False + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return join_description + + def get_instance_description_short(self) -> Union[str, Sequence]: + return view_description, self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + #self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + + if self.slow_motion: + t_scale = 0.4 + light_y = 50 + else: + t_scale = 1.0 + light_y = 150 + lstart = 7.1 * t_scale + inc = 1.25 * t_scale + + ba.timer(lstart, self._do_light_1) + ba.timer(lstart + inc, self._do_light_2) + ba.timer(lstart + 2 * inc, self._do_light_3) + ba.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = ba.newnode('image', + attrs={ + 'texture': ba.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + ba.animate( + lnub, 'opacity', { + 4.0 * t_scale: 0, + 5.0 * t_scale: 1.0, + 12.0 * t_scale: 1.0, + 12.5 * t_scale: 0.0 + }) + ba.timer(13.0 * t_scale, lnub.delete) + self._start_lights.append(lnub) + + self._start_lights[0].color = (0.2, 0, 0) + self._start_lights[1].color = (0.2, 0, 0) + self._start_lights[2].color = (0.2, 0.05, 0) + self._start_lights[3].color = (0.0, 0.3, 0) + + def _do_light_1(self) -> None: + assert self._start_lights is not None + self._start_lights[0].color = (1.0, 0, 0) + ba.playsound(self._beep_1_sound) + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + ba.playsound(self._beep_1_sound) + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + ba.playsound(self._beep_1_sound) + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + ba.playsound(self._beep_2_sound) + + self._started = True + + for player in self.players: + self.generate_mines(player) + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_pickup=False) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + if self._started: + self.generate_mines(player) + return spaz + + + def generate_mines(self,player: Player): + try: + player.actived = ba.Timer(0.5,ba.Call(self.spawn_mine, player),repeat=True) + except Exception as e: + print('Exception -> '+ str(e)) + + + + def spawn_mine(self,player: Player): + if player.team.score >= self._score_to_win: + return + pos = player.actor.node.position + # mine = stdbomb.Bomb(position=(pos[0], pos[1] + 2.0, pos[2]), + # velocity=(0, 0, 0), + # bomb_type='land_mine', + # #blast_radius=, + # source_player=player.actor.source_player, + # owner=player.actor.node).autoretain() + mine = Custom_Mine(position=(pos[0], pos[1] + 2.0, pos[2]), + source_player=player.actor.source_player) + + def arm(): + mine.arm() + ba.timer(0.5,arm) + + player.mines.append(mine) + if len(player.mines) > 15: + for m in player.mines: + try: + m.node.delete() + except Exception: + pass + player.mines.remove(m) + break + + self.handlemessage(ScoreMessage(player)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + player.actived = None + + elif isinstance(msg, ScoreMessage): + player = msg.getplayer() + + player.team.score += 1 + self._update_scoreboard() + + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + self.end_game() #ba.timer(0.5, self.end_game) + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/plugins/minigames/ufo_fight.py b/plugins/minigames/ufo_fight.py new file mode 100644 index 0000000..29c4571 --- /dev/null +++ b/plugins/minigames/ufo_fight.py @@ -0,0 +1,994 @@ +"""UFO Boss Fight v1.0: +Made by Cross Joy""" + +# Anyone who wanna help me in giving suggestion/ fix bugs/ by creating PR, +# Can visit my github https://github.com/CrossJoy/Bombsquad-Modding + +# You can contact me through discord: +# My Discord Id: Cross Joy#0721 +# My BS Discord Server: https://discford.gg/JyBY6haARJ + +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +import ba, _ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.spaz import Spaz +from bastd.actor.bomb import Blast, Bomb +from bastd.actor.onscreentimer import OnScreenTimer +from bastd.actor.spazbot import SpazBotSet, StickyBot +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Union, Callable + + +class UFODiedMessage: + ufo: UFO + """The UFO that was killed.""" + + killerplayer: ba.Player | None + """The ba.Player that killed it (or None).""" + + how: ba.DeathType + """The particular type of death.""" + + def __init__( + self, + ufo: UFO, + killerplayer: ba.Player | None, + how: ba.DeathType, + ): + """Instantiate with given values.""" + self.spazbot = ufo + self.killerplayer = killerplayer + self.how = how + + +class RoboBot(StickyBot): + character = 'B-9000' + default_bomb_type = 'land_mine' + color = (0, 0, 0) + highlight = (3, 3, 3) + + + + +class UFO(ba.Actor): + """ + New AI for Boss + """ + + # pylint: disable=too-many-public-methods + # pylint: disable=too-many-locals + + node: ba.Node + + def __init__(self, hitpoints: int = 5000): + + super().__init__() + shared = SharedObjects.get() + + self.update_callback: Callable[[UFO], Any] | None = None + activity = self.activity + assert isinstance(activity, ba.GameActivity) + + self.platform_material = ba.Material() + self.platform_material.add_actions( + conditions=('they_have_material', shared.footing_material), + actions=( + 'modify_part_collision', 'collide', True)) + self.ice_material = ba.Material() + self.ice_material.add_actions( + actions=('modify_part_collision', 'friction', 0.0)) + + self._player_pts: list[tuple[ba.Vec3, ba.Vec3]] | None = None + self._ufo_update_timer: ba.Timer | None = None + self.last_player_attacked_by: ba.Player | None = None + self.last_attacked_time = 0.0 + self.last_attacked_type: tuple[str, str] | None = None + + self.to_target: ba.Vec3 = ba.Vec3(0, 0, 0) + self.dist = (0, 0, 0) + + self._bots = SpazBotSet() + self.frozen = False + self.y_pos = 3 + self.xz_pos = 1 + self.bot_count = 3 + self.bot_dur_froze = False + + self.hitpoints = hitpoints + self.hitpoints_max = hitpoints + self._width = 240 + self._width_max = 240 + self._height = 35 + self._bar_width = 240 + self._bar_height = 35 + self._bar_tex = self._backing_tex = ba.gettexture('bar') + self._cover_tex = ba.gettexture('uiAtlas') + self._model = ba.getmodel('meterTransparent') + self.bar_posx = -120 + + self._last_hit_time: int | None = None + self.impact_scale = 1.0 + self._num_times_hit = 0 + + self._sucker_mat = ba.Material() + + self.ufo_material = ba.Material() + self.ufo_material.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=(('modify_node_collision', 'collide', True), + ('modify_part_collision', 'physical', True))) + + self.ufo_material.add_actions( + conditions=(('they_have_material', + shared.object_material), 'or', + ('they_have_material', + shared.footing_material), 'or', + ('they_have_material', + self.ufo_material)), + actions=('modify_part_collision', 'physical', False)) + + activity = _ba.get_foreground_host_activity() + with ba.Context(activity): + point = activity.map.get_flag_position(None) + boss_spawn_pos = (point[0], point[1] + 1, point[2]) + + self.node = ba.newnode('prop', delegate=self, attrs={ + 'position': boss_spawn_pos, + 'velocity': (2, 0, 0), + 'color_texture': ba.gettexture('achievementFootballShutout'), + 'model': ba.getmodel('landMine'), + # 'light_model': ba.getmodel('powerupSimple'), + 'model_scale': 3.3, + 'body': 'landMine', + 'body_scale': 3.3, + 'gravity_scale': 0.05, + 'density': 1, + 'reflection': 'soft', + 'reflection_scale': [0.25], + 'shadow_size': 0.1, + 'max_speed': 1.5, + 'is_area_of_interest': + True, + 'materials': [shared.footing_material, shared.object_material]}) + + self.holder = ba.newnode('region', attrs={ + 'position': ( + boss_spawn_pos[0], boss_spawn_pos[1] - 0.25, + boss_spawn_pos[2]), + 'scale': [6, 0.1, 2.5 - 0.1], + 'type': 'box', + 'materials': (self.platform_material, self.ice_material, + shared.object_material)}) + + self.suck_anim = ba.newnode('locator', + owner=self.node, + attrs={'shape': 'circleOutline', + 'position': ( + boss_spawn_pos[0], + boss_spawn_pos[1] - 0.25, + boss_spawn_pos[2]), + 'color': (4, 4, 4), + 'opacity': 1.0, + 'draw_beauty': True, + 'additive': True}) + + def suck_anim(): + ba.animate_array(self.suck_anim, 'position', 3, + {0: ( + self.node.position[0], + self.node.position[1] - 5, + self.node.position[2]), + 0.5: ( + self.node.position[ + 0] + self.to_target.x / 2, + self.node.position[ + 1] + self.to_target.y / 2, + self.node.position[ + 2] + self.to_target.z / 2)}) + + self.suck_timer = ba.Timer(0.5, suck_anim, repeat=True) + + self.blocks = [] + + self._sucker_mat.add_actions( + conditions=( + ('they_have_material', shared.player_material) + ), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._levitate) + + )) + + # self.sucker = ba.newnode('region', attrs={ + # 'position': ( + # boss_spawn_pos[0], boss_spawn_pos[1] - 2, boss_spawn_pos[2]), + # 'scale': [2, 10, 2], + # 'type': 'box', + # 'materials': self._sucker_mat, }) + + self.suck = ba.newnode('region', + attrs={'position': ( + boss_spawn_pos[0], boss_spawn_pos[1] - 2, + boss_spawn_pos[2]), + 'scale': [1, 10, 1], + 'type': 'box', + 'materials': [self._sucker_mat]}) + + self.node.connectattr('position', self.holder, 'position') + self.node.connectattr('position', self.suck, 'position') + + ba.animate(self.node, 'model_scale', { + 0: 0, + 0.2: self.node.model_scale * 1.1, + 0.26: self.node.model_scale}) + + self.shield_deco = ba.newnode('shield', owner=self.node, + attrs={'color': (4, 4, 4), + 'radius': 1.2}) + self.node.connectattr('position', self.shield_deco, 'position') + self._scoreboard() + self._update() + self.drop_bomb_timer = ba.Timer(1.5, ba.Call(self._drop_bomb), + repeat=True) + + self.drop_bots_timer = ba.Timer(15.0, ba.Call(self._drop_bots), repeat=True) + + def _drop_bots(self) -> None: + p = self.node.position + if not self.frozen: + for i in range(self.bot_count): + ba.timer( + 1.0 + i, + lambda: self._bots.spawn_bot( + RoboBot, pos=(self.node.position[0], + self.node.position[1] - 1, + self.node.position[2]), spawn_time=0.0 + ), + ) + else: + self.bot_dur_froze = True + + def _drop_bomb(self) -> None: + t = self.to_target + p = self.node.position + if not self.frozen: + if abs(self.dist[0]) < 2 and abs(self.dist[2]) < 2: + Bomb(position=(p[0], p[1] - 0.5, p[2]), + velocity=(t[0] * 5, 0, t[2] * 5), + bomb_type='land_mine').autoretain().arm() + elif self.hitpoints > self.hitpoints_max * 3 / 4: + Bomb(position=(p[0], p[1] - 1.5, p[2]), + velocity=(t[0] * 8, 2, t[2] * 8), + bomb_type='normal').autoretain() + elif self.hitpoints > self.hitpoints_max * 1 / 2: + Bomb(position=(p[0], p[1] - 1.5, p[2]), + velocity=(t[0] * 8, 2, t[2] * 8), + bomb_type='ice').autoretain() + + elif self.hitpoints > self.hitpoints_max * 1 / 4: + Bomb(position=(p[0], p[1] - 1.5, p[2]), + velocity=(t[0] * 15, 2, t[2] * 15), + bomb_type='sticky').autoretain() + else: + Bomb(position=(p[0], p[1] - 1.5, p[2]), + velocity=(t[0] * 15, 2, t[2] * 15), + bomb_type='impact').autoretain() + + def _levitate(self): + node = ba.getcollision().opposingnode + if node.exists(): + p = node.getdelegate(Spaz, True) + + def raise_player(player: ba.Player): + if player.is_alive(): + node = player.node + try: + node.handlemessage("impulse", node.position[0], + node.position[1] + .5, + node.position[2], 0, 5, 0, 3, 10, 0, + 0, 0, 5, 0) + + + except: + pass + + if not self.frozen: + for i in range(7): + ba.timer(0.05 + i / 20, ba.Call(raise_player, p)) + + def on_punched(self, damage: int) -> None: + """Called when this spaz gets punched.""" + + def do_damage(self, msg: Any) -> None: + if not self.node: + return None + + + damage = abs(msg.magnitude) + if msg.hit_type == 'explosion': + damage /= 20 + else: + damage /= 5 + + self.hitpoints -= int(damage) + if self.hitpoints <= 0: + self.handlemessage(ba.DieMessage()) + + def _get_target_player_pt(self) -> tuple[ + ba.Vec3 | None, ba.Vec3 | None]: + """Returns the position and velocity of our target. + + Both values will be None in the case of no target. + """ + assert self.node + botpt = ba.Vec3(self.node.position) + closest_dist: float | None = None + closest_vel: ba.Vec3 | None = None + closest: ba.Vec3 | None = None + assert self._player_pts is not None + for plpt, plvel in self._player_pts: + dist = (plpt - botpt).length() + + # Ignore player-points that are significantly below the bot + # (keeps bots from following players off cliffs). + if (closest_dist is None or dist < closest_dist) and ( + plpt[1] > botpt[1] - 5.0 + ): + closest_dist = dist + closest_vel = plvel + closest = plpt + if closest_dist is not None: + assert closest_vel is not None + assert closest is not None + return ( + ba.Vec3(closest[0], closest[1], closest[2]), + ba.Vec3(closest_vel[0], closest_vel[1], closest_vel[2]), + ) + return None, None + + def set_player_points(self, pts: list[tuple[ba.Vec3, ba.Vec3]]) -> None: + """Provide the spaz-bot with the locations of its enemies.""" + self._player_pts = pts + + def exists(self) -> bool: + return bool(self.node) + + def show_damage_count(self, damage: str, position: Sequence[float], + direction: Sequence[float]) -> None: + """Pop up a damage count at a position in space. + + Category: Gameplay Functions + """ + lifespan = 1.0 + app = ba.app + + # FIXME: Should never vary game elements based on local config. + # (connected clients may have differing configs so they won't + # get the intended results). + do_big = app.ui.uiscale is ba.UIScale.SMALL or app.vr_mode + txtnode = ba.newnode('text', + attrs={ + 'text': damage, + 'in_world': True, + 'h_align': 'center', + 'flatness': 1.0, + 'shadow': 1.0 if do_big else 0.7, + 'color': (1, 0.25, 0.25, 1), + 'scale': 0.035 if do_big else 0.03 + }) + # Translate upward. + tcombine = ba.newnode('combine', owner=txtnode, attrs={'size': 3}) + tcombine.connectattr('output', txtnode, 'position') + v_vals = [] + pval = 0.0 + vval = 0.07 + count = 6 + for i in range(count): + v_vals.append((float(i) / count, pval)) + pval += vval + vval *= 0.5 + p_start = position[0] + p_dir = direction[0] + ba.animate(tcombine, 'input0', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + p_start = position[1] + p_dir = direction[1] + ba.animate(tcombine, 'input1', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + p_start = position[2] + p_dir = direction[2] + ba.animate(tcombine, 'input2', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + ba.animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0}) + ba.timer(lifespan, txtnode.delete) + + def _scoreboard(self) -> None: + self._backing = ba.NodeActor( + ba.newnode('image', + attrs={ + 'position': (self.bar_posx + self._width / 2, -100), + 'scale': (self._width, self._height), + 'opacity': 0.7, + 'color': (0.3, + 0.3, + 0.3), + 'vr_depth': -3, + 'attach': 'topCenter', + 'texture': self._backing_tex + })) + self._bar = ba.NodeActor( + ba.newnode('image', + attrs={ + 'opacity': 1.0, + 'color': (0.5, 0.5, 0.5), + 'attach': 'topCenter', + 'texture': self._bar_tex + })) + self._bar_scale = ba.newnode('combine', + owner=self._bar.node, + attrs={ + 'size': 2, + 'input0': self._bar_width, + 'input1': self._bar_height + }) + self._bar_scale.connectattr('output', self._bar.node, 'scale') + self._bar_position = ba.newnode( + 'combine', + owner=self._bar.node, + attrs={ + 'size': 2, + 'input0': self.bar_posx + self._bar_width / 2, + 'input1': -100 + }) + self._bar_position.connectattr('output', self._bar.node, 'position') + self._cover = ba.NodeActor( + ba.newnode('image', + attrs={ + 'position': (self.bar_posx + 120, -100), + 'scale': + (self._width * 1.15, self._height * 1.6), + 'opacity': 1.0, + 'color': (0.3, + 0.3, + 0.3), + 'vr_depth': 2, + 'attach': 'topCenter', + 'texture': self._cover_tex, + 'model_transparent': self._model + })) + self._score_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'position': (self.bar_posx + 120, -100), + 'h_attach': 'center', + 'v_attach': 'top', + 'h_align': 'center', + 'v_align': 'center', + 'maxwidth': 130, + 'scale': 0.9, + 'text': '', + 'shadow': 0.5, + 'flatness': 1.0, + 'color': (1, 1, 1, 0.8) + })) + + def _update(self) -> None: + self._score_text.node.text = str(self.hitpoints) + self._bar_width = self.hitpoints * self._width_max / self.hitpoints_max + cur_width = self._bar_scale.input0 + ba.animate(self._bar_scale, 'input0', { + 0.0: cur_width, + 0.1: self._bar_width + }) + cur_x = self._bar_position.input0 + + ba.animate(self._bar_position, 'input0', { + 0.0: cur_x, + 0.1: self.bar_posx + self._bar_width / 2 + }) + + if self.hitpoints > self.hitpoints_max * 3 / 4: + ba.animate_array(self.shield_deco, 'color', 3, + {0: self.shield_deco.color, 0.2: (4, 4, 4)}) + elif self.hitpoints > self.hitpoints_max * 1 / 2: + ba.animate_array(self.shield_deco, 'color', 3, + {0: self.shield_deco.color, 0.2: (3, 3, 5)}) + self.bot_count = 4 + + elif self.hitpoints > self.hitpoints_max * 1 / 4: + ba.animate_array(self.shield_deco, 'color', 3, + {0: self.shield_deco.color, 0.2: (1, 5, 1)}) + self.bot_count = 5 + + else: + ba.animate_array(self.shield_deco, 'color', 3, + {0: self.shield_deco.color, 0.2: (5, 0.2, 0.2)}) + self.bot_count = 6 + + + def update_ai(self) -> None: + """Should be called periodically to update the spaz' AI.""" + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + if self.update_callback is not None: + if self.update_callback(self): + # Bot has been handled. + return + + if not self.node: + return + + pos = self.node.position + our_pos = ba.Vec3(pos[0], pos[1] - self.y_pos, pos[2]) + + target_pt_raw: ba.Vec3 | None + target_vel: ba.Vec3 | None + + target_pt_raw, target_vel = self._get_target_player_pt() + + try: + dist_raw = (target_pt_raw - our_pos).length() + + target_pt = ( + target_pt_raw + target_vel * dist_raw * 0.3 + ) + except: + return + diff = target_pt - our_pos + # self.dist = diff.length() + self.dist = diff + self.to_target = diff.normalized() + + # p = spaz.node.position + # pt = self.getTargetPosition(p) + # pn = self.node.position + # d = [pt[0] - pn[0], pt[1] - pn[1], pt[2] - pn[2]] + # speed = self.getMaxSpeedByDir(d) + # self.node.velocity = (self.to_target.x, self.to_target.y, self.to_target.z) + if self.hitpoints == 0: + setattr(self.node, 'velocity', + (0, self.to_target.y, 0)) + setattr(self.node, 'extra_acceleration', + (0, self.to_target.y * 80 + 70, + 0)) + else: + setattr(self.node, 'velocity', + (self.to_target.x * self.xz_pos, + self.to_target.y, + self.to_target.z * self.xz_pos)) + setattr(self.node, 'extra_acceleration', + (self.to_target.x * self.xz_pos , + self.to_target.y * 80 + 70, + self.to_target.z * self.xz_pos)) + + def on_expire(self) -> None: + super().on_expire() + + # We're being torn down; release our callback(s) so there's + # no chance of them keeping activities or other things alive. + self.update_callback = None + + def animate_model(self) -> None: + if not self.node: + return None + # ba.animate(self.node, 'model_scale', { + # 0: self.node.model_scale, + # 0.08: self.node.model_scale * 0.9, + # 0.15: self.node.model_scale}) + ba.emitfx(position=self.node.position, + velocity=self.node.velocity, + count=int(6 + random.random() * 10), + scale=0.5, + spread=0.4, + chunk_type='metal') + + def handlemessage(self, msg: Any) -> Any: + # pylint: disable=too-many-branches + assert not self.expired + + if isinstance(msg, ba.HitMessage): + # Don't die on punches (that's annoying). + self.animate_model() + if self.hitpoints != 0: + self.do_damage(msg) + # self.show_damage_msg(msg) + self._update() + + elif isinstance(msg, ba.DieMessage): + if self.node: + self.hitpoints = 0 + self.frozen = True + self.suck_timer = False + self.drop_bomb_timer = False + self.drop_bots_timer = False + + p = self.node.position + + for i in range(6): + def ded_explode(count): + p_x = p[0] + random.uniform(-1, 1) + p_z = p[2] + random.uniform(-1, 1) + if count == 5: + Blast( + position=(p[0], p[1], p[2]), + blast_type='tnt', + blast_radius=5.0).autoretain() + else: + Blast( + position=(p_x, p[1], p_z), + blast_radius=2.0).autoretain() + + ba.timer(0 + i, ba.Call(ded_explode, i)) + + ba.timer(5, self.node.delete) + ba.timer(0.1, self.suck.delete) + ba.timer(0.1, self.suck_anim.delete) + + elif isinstance(msg, ba.OutOfBoundsMessage): + activity = _ba.get_foreground_host_activity() + try: + point = activity.map.get_flag_position(None) + boss_spawn_pos = (point[0], point[1] + 1.5, point[2]) + assert self.node + self.node.position = boss_spawn_pos + except: + self.handlemessage(ba.DieMessage()) + + elif isinstance(msg, ba.FreezeMessage): + if not self.frozen: + self.frozen = True + self.y_pos = -1.5 + self.xz_pos = 0.01 + self.node.reflection_scale = [2] + + + def unfrozen(): + self.frozen = False + if self.bot_dur_froze: + ba.timer(0.1, ba.Call(self._drop_bots)) + self.bot_dur_froze = False + self.y_pos = 3 + self.xz_pos = 1 + self.node.reflection_scale = [0.25] + + + ba.timer(5.0, unfrozen) + + else: + super().handlemessage(msg) + + +class UFOSet: + """A container/controller for one or more ba.SpazBots. + + category: Bot Classes + """ + + def __init__(self) -> None: + """Create a bot-set.""" + + # We spread our bots out over a few lists so we can update + # them in a staggered fashion. + self._ufo_bot_list_count = 5 + self._ufo_bot_add_list = 0 + self._ufo_bot_update_list = 0 + self._ufo_bot_lists: list[list[UFO]] = [ + [] for _ in range(self._ufo_bot_list_count) + ] + self._ufo_spawn_sound = ba.getsound('spawn') + self._ufo_spawning_count = 0 + self._ufo_bot_update_timer: ba.Timer | None = None + self.start_moving() + + 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._ufo_bot_lists[self._ufo_bot_update_list] = [ + b for b in self._ufo_bot_lists[self._ufo_bot_update_list] if b + ] + except Exception: + bot_list = [] + ba.print_exception( + 'Error updating bot list: ' + + str(self._ufo_bot_lists[self._ufo_bot_update_list]) + ) + self._bot_update_list = ( + self._ufo_bot_update_list + 1 + ) % self._ufo_bot_list_count + + # Update our list of player points for the bots to use. + player_pts = [] + for player in ba.getactivity().players: + assert isinstance(player, ba.Player) + try: + # TODO: could use abstracted player.position here so we + # don't have to assume their actor type, but we have no + # abstracted velocity as of yet. + if player.is_alive(): + assert isinstance(player.actor, UFO) + assert player.actor.node + player_pts.append( + ( + ba.Vec3(player.actor.node.position), + ba.Vec3(player.actor.node.velocity), + ) + ) + except Exception: + ba.print_exception('Error on bot-set _update.') + + for bot in bot_list: + bot.set_player_points(player_pts) + bot.update_ai() + + def start_moving(self) -> None: + """Start processing bot AI updates so they start doing their thing.""" + self._ufo_bot_update_timer = ba.Timer( + 0.05, ba.WeakCall(self._update), repeat=True + ) + + def spawn_bot( + self, + bot_type: type[UFO], + pos: Sequence[float], + spawn_time: float = 3.0, + on_spawn_call: Callable[[UFO], Any] | None = None, + ) -> None: + """Spawn a bot from this set.""" + from bastd.actor import spawner + + spawner.Spawner( + pt=pos, + spawn_time=spawn_time, + send_spawn_message=False, + spawn_callback=ba.Call( + self._spawn_bot, bot_type, pos, on_spawn_call + ), + ) + self._ufo_spawning_count += 1 + + def _spawn_bot( + self, + bot_type: type[UFO], + pos: Sequence[float], + on_spawn_call: Callable[[UFO], Any] | None, + ) -> None: + spaz = bot_type() + ba.playsound(self._ufo_spawn_sound, position=pos) + assert spaz.node + spaz.node.handlemessage('flash') + spaz.node.is_area_of_interest = False + spaz.handlemessage(ba.StandMessage(pos, random.uniform(0, 360))) + self.add_bot(spaz) + self._ufo_spawning_count -= 1 + if on_spawn_call is not None: + on_spawn_call(spaz) + + def add_bot(self, bot: UFO) -> None: + """Add a ba.SpazBot instance to the set.""" + self._ufo_bot_lists[self._ufo_bot_add_list].append(bot) + self._ufo_bot_add_list = ( + self._ufo_bot_add_list + 1) % self._ufo_bot_list_count + + def have_living_bots(self) -> bool: + """Return whether any bots in the set are alive or spawning.""" + return self._ufo_spawning_count > 0 or any( + any(b.is_alive() for b in l) for l in self._ufo_bot_lists + ) + + def get_living_bots(self) -> list[UFO]: + """Get the living bots in the set.""" + bots: list[UFO] = [] + for botlist in self._ufo_bot_lists: + for bot in botlist: + if bot.is_alive(): + bots.append(bot) + return bots + + def clear(self) -> None: + """Immediately clear out any bots in the set.""" + + # Don't do this if the activity is shutting down or dead. + activity = ba.getactivity(doraise=False) + if activity is None or activity.expired: + return + + for i, bot_list in enumerate(self._ufo_bot_lists): + for bot in bot_list: + bot.handlemessage(ba.DieMessage(immediate=True)) + self._ufo_bot_lists[i] = [] + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export game +class UFOightGame(ba.TeamGameActivity[Player, Team]): + """ + A co-op game where you try to defeat UFO Boss + as fast as possible + """ + + name = 'UFO Fight' + description = 'REal Boss Fight?' + scoreconfig = ba.ScoreConfig( + label='Time', scoretype=ba.ScoreType.MILLISECONDS, lower_is_better=True + ) + default_music = ba.MusicType.TO_THE_DEATH + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + # For now we're hard-coding spawn positions and whatnot + # so we need to be sure to specify that we only support + # a specific map. + return ['Football Stadium'] + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + # We currently support Co-Op only. + return issubclass(sessiontype, ba.CoopSession) + + # In the constructor we should load any media we need/etc. + # ...but not actually create anything yet. + def __init__(self, settings: dict): + super().__init__(settings) + self._winsound = ba.getsound('score') + self._won = False + self._timer: OnScreenTimer | None = None + self._bots = UFOSet() + self._preset = str(settings['preset']) + self._credit = ba.newnode('text', + attrs={ + 'v_attach': 'bottom', + 'h_align': 'center', + 'color': (0.4, 0.4, 0.4), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, 20), + 'scale': 0.7, + 'text': 'By Cross Joy' + }) + + + + def on_transition_in(self) -> None: + super().on_transition_in() + gnode = ba.getactivity().globalsnode + gnode.tint = (0.42, 0.55, 0.66) + + # Called when our game actually begins. + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_powerup_drops() + + + + # In pro mode there's no powerups. + + # Make our on-screen timer and start it roughly when our bots appear. + self._timer = OnScreenTimer() + ba.timer(4.0, self._timer.start) + + def checker(): + if not self._won: + self.timer = ba.Timer(0.1, self._check_if_won, repeat=True) + + ba.timer(10, checker) + activity = _ba.get_foreground_host_activity() + + point = activity.map.get_flag_position(None) + boss_spawn_pos = (point[0], point[1] + 1.5, point[2]) + + # Spawn some baddies. + ba.timer( + 1.0, + lambda: self._bots.spawn_bot( + UFO, pos=boss_spawn_pos, spawn_time=3.0 + ), + ) + + # Called for each spawning player. + + def _check_if_won(self) -> None: + # Simply end the game if there's no living bots. + # FIXME: Should also make sure all bots have been spawned; + # if spawning is spread out enough that we're able to kill + # all living bots before the next spawns, it would incorrectly + # count as a win. + if not self._bots.have_living_bots(): + self.timer = False + self._won = True + self.end_game() + + # Called for miscellaneous messages. + def handlemessage(self, msg: Any) -> Any: + + # A player has died. + if isinstance(msg, ba.PlayerDiedMessage): + player = msg.getplayer(Player) + self.stats.player_was_killed(player) + ba.timer(0.1, self._checkroundover) + + # A spaz-bot has died. + elif isinstance(msg, UFODiedMessage): + # Unfortunately the ufo 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. + ba.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 _checkroundover(self) -> None: + """End the round if conditions are met.""" + if not any(player.is_alive() for player in self.teams[0].players): + self.end_game() + + 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 = ba.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((ba.time() - self._timer.starttime) * 1000.0) + ba.cameraflash() + ba.playsound(self._winsound) + for team in self.teams: + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage()) + results.set_team_score(team, elapsed_time_ms) + + # Ends the activity. + self.end(results) + + +# ba_meta export plugin +class MyUFOFightLevel(ba.Plugin): + + def on_app_running(self) -> None: + ba.app.add_coop_practice_level( + ba.Level( + name='The UFO Fight', + displayname='${GAME}', + gametype=UFOightGame, + settings={'preset': 'regular'}, + preview_texture_name='footballStadiumPreview', + ) + ) + diff --git a/plugins/minigames/yeeting_party.py b/plugins/minigames/yeeting_party.py new file mode 100644 index 0000000..8ae6721 --- /dev/null +++ b/plugins/minigames/yeeting_party.py @@ -0,0 +1,222 @@ +#Made by your friend: @[Just] Freak#4999 + +# ba_meta require api 7 +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class BoxingGame(ba.TeamGameActivity[Player, Team]): + """A game of yeeting people out of map""" + + name = 'Yeeting Party!' + description = 'Yeet your enemies out of the map' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, ba.FreeForAllSession): + settings.append( + ba.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Bridgit', 'Rampage', 'Monkey Face'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Yeet ${ARG1} enemies out of the map!', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'yeet ${ARG1} enemies', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + def getRandomPowerupPoint(self) -> None: + myMap = self.map.getname() + if myMap == 'Doom Shroom': + while True: + x = random.uniform(-1.0,1.0) + y = random.uniform(-1.0,1.0) + if x*x+y*y < 1.0: break + return ((8.0*x,2.5,-3.5+5.0*y)) + elif myMap == 'Rampage': + x = random.uniform(-6.0,7.0) + y = random.uniform(-6.0,-2.5) + return ((x, 5.2, y)) + else: + x = random.uniform(-5.0,5.0) + y = random.uniform(-6.0,0.0) + return ((x, 8.0, y)) + + def on_begin(self) -> None: + super().on_begin() + ba.screenmessage("start Yeeting",color = (0.2,1,1)) + self.setup_standard_time_limit(self._time_limit) + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + def spawn_player(self, player: Player) -> ba.Actor: + + spaz = self.spawn_player_spaz(player) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player(enable_punch=False, + enable_jump=True, + enable_bomb=False, + enable_pickup=True) + return spaz + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, ba.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + ba.playsound(self._dingsound) + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + ba.playsound(self._dingsound) + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + ba.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results)