# ba_meta require api 8 # (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 babase import bauiv1 as bui import bascenev1 as bs import random from bascenev1lib.actor.flag import Flag from bascenev1lib.actor.popuptext import PopupText from bascenev1lib.actor.playerspaz import PlayerSpaz from bascenev1lib.actor.scoreboard import Scoreboard from bascenev1lib.gameutils import SharedObjects if TYPE_CHECKING: from typing import Any, Sequence lang = bs.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(bs.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(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: self.score = 0 # ba_meta export bascenev1.GameActivity class CollectorGame(bs.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[bs.Session] ) -> list[babase.Setting]: settings = [ bs.IntSetting( capsules_to_win, min_value=1, default=10, increment=1, ), bs.IntSetting( capsules_death, min_value=1, max_value=10, default=2, increment=1, ), bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), bs.BoolSetting(lucky_capsules, default=True), bs.BoolSetting('Epic Mode', default=False), ] return settings @classmethod def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: return issubclass(sessiontype, bs.DualTeamSession) or issubclass( sessiontype, bs.FreeForAllSession ) @classmethod def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: return bs.app.classic.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 = bs.getsound('swip') self._lucky_sound = bs.getsound('ding') self._flag_pos: Sequence[float] | None = None self._flag_state: FlagState | None = None self._flag: Flag | None = None self._flag_light: bs.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_mesh = bs.getmesh('bomb') self._capsule_tex = bs.gettexture('bombColor') self._capsule_lucky_tex = bs.gettexture('bombStickyColor') self._collect_sound = bs.getsound('powerup01') self._lucky_collect_sound = bs.getsound('cashRegister2') self._capsule_material = bs.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 = bs.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', babase.Call(self._handle_player_flag_region_collide, True), ), ( 'call', 'at_disconnect', babase.Call(self._handle_player_flag_region_collide, False), ), ), ) # Base class overrides. self.slow_motion = self._epic_mode self.default_music = ( bs.MusicType.EPIC if self._epic_mode else bs.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: bs.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, bs.FreeForAllSession): self._flag_pos = self.map.get_flag_position(random.randint(0, 1)) else: self._flag_pos = self.map.get_flag_position(None) bs.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 = bs.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] bs.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, bs.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) self._collect_sound.play(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 = bs.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 = babase.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: self._swipsound.play() def _handle_player_flag_region_collide(self, colliding: bool) -> None: try: spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True) except bs.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 = bs.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 bs.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() self._lucky_collect_sound.play(1.0, position=capsule.node.position) bs.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') bs.emitfx( position=capsule.node.position, velocity=(0, 0, 0), count=int(4.0+random.random()*6), emit_type='tendrils') else: player.capsules += 1 self._collect_sound.play(0.6, position=capsule.node.position) # create a flash light = bs.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) bs.animate(light, 'intensity', { 0: 0, 0.1: 0.5, 0.2: 0 }, loop=False) bs.timer(0.2, light.delete) capsule.handlemessage(bs.DieMessage()) def _update_player_light(self, player: Player, capsules: int) -> None: if player.light: intensity = 0.04 * capsules bs.animate(player.light, 'intensity', { 0.0: player.light.intensity, 0.1: intensity }) def newintensity(): player.light.intensity = intensity bs.timer(0.1, newintensity) else: player.light = bs.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, bs.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(bs.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: activity._lucky_sound.play(1.0, self._spawn_pos) self.node = bs.newnode( 'prop', attrs={ 'mesh': activity._capsule_mesh, '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) bs.animate(self.node, 'mesh_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 = bs.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, bs.DieMessage): self.node.delete() bs.animate(self._light_capsule, 'intensity', { 0: 1.0, 0.05: 0.0 }, loop=False) bs.timer(0.05, self._light_capsule.delete) elif isinstance(msg, bs.OutOfBoundsMessage): self.handlemessage(bs.DieMessage()) elif isinstance(msg, bs.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)