diff --git a/plugins/minigames.json b/plugins/minigames.json index 5f3b9e2..01a7b96 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -877,6 +877,104 @@ "md5sum": "1cbe5b3e85b5dfcee1eb322f33568fd4" } } + }, + "Avalanche": { + "description": "Dodge the falling ice bombs", + "external_url": "", + "authors": [ + { + "name": "", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } + }, + "HYPER_RACE": { + "description": "Race and avoid the obsatacles", + "external_url": "", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.0": null + } + }, + "meteorshowerdeluxe": { + "description": "Meteor shower on all maps support", + "external_url": "", + "authors": [ + { + "name": "EraOSBeta", + "email": "", + "discord": "3ra0" + } + ], + "versions": { + "1.0.0": null + } + }, + "ofuuuAttack": { + "description": "Dodge the falling bombs.", + "external_url": "", + "authors": [ + { + "name": "Riyukii", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } + }, + "safe_zone": { + "description": "Stay in the safe zone", + "external_url": "", + "authors": [ + { + "name": "SEBASTIAN2059", + "email": "", + "discord": "sebastian2059" + } + ], + "versions": { + "1.0.0": null + } + }, + "SnowBallFight": { + "description": "Throw snoballs and dominate", + "external_url": "https://youtu.be/uXyb_meBjGI?si=D_N_OXZT5BFh8R5C", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.0": null + } + }, + "EggGame": { + "description": "Throw Egg as far u can", + "external_url": "https://youtu.be/82vLp9ceCcw?si=OSC5Hu3Ns7PevlwP", + "authors": [ + { + "name": "Mr.Smoothy", + "email": "", + "discord": "mr.smoothy" + } + ], + "versions": { + "1.0.0": null + } } } } \ No newline at end of file diff --git a/plugins/minigames/Avalanche.py b/plugins/minigames/Avalanche.py new file mode 100644 index 0000000..1e36208 --- /dev/null +++ b/plugins/minigames/Avalanche.py @@ -0,0 +1,143 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +"""Avalancha mini-game.""" + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations +import random +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.bomb import Bomb +from bascenev1lib.actor.onscreentimer import OnScreenTimer +from bascenev1lib.game.meteorshower import * +from bascenev1lib.actor.spazbot import * +from bascenev1lib.actor.spaz import PunchHitMessage +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List, Dict, Type, Type + +## MoreMinigames.py support ## +randomPic = ["lakeFrigidPreview", "hockeyStadiumPreview"] + + +def ba_get_api_version(): + return 6 + + +def ba_get_levels(): + return [ + babase._level.Level( + "Icy Emits", + gametype=IcyEmitsGame, + settings={}, + preview_texture_name=random.choice(randomPic), + ) + ] + + +## MoreMinigames.py support ## + + +class PascalBot(BrawlerBot): + color = (0, 0, 3) + highlight = (0.2, 0.2, 1) + character = "Pascal" + bouncy = True + punchiness = 0.7 + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, bs.FreezeMessage): + return + else: + super().handlemessage(msg) + + +# ba_meta export bascenev1.GameActivity +class AvalanchaGame(MeteorShowerGame): + """Minigame involving dodging falling bombs.""" + + name = "Avalanche" + description = "Dodge the ice-bombs." + available_settings = [ + bs.BoolSetting("Epic Mode", default=False), + bs.IntSetting("Difficulty", default=1, min_value=1, max_value=3, increment=1), + ] + scoreconfig = bs.ScoreConfig( + label="Survived", scoretype=bs.ScoreType.MILLISECONDS, version="B" + ) + + announce_player_deaths = True + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ["Tip Top"] + + def __init__(self, settings: dict): + super().__init__(settings) + + self._epic_mode = settings.get("Epic Mode", False) + self._last_player_death_time: Optional[float] = None + self._meteor_time = 2.0 + if settings["Difficulty"] == 1: + self._min_delay = 0.4 + elif settings["Difficulty"] == 2: + self._min_delay = 0.3 + else: + self._min_delay = 0.1 + + self._timer: Optional[OnScreenTimer] = None + self._bots = SpazBotSet() + + self.default_music = ( + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL + ) + if self._epic_mode: + self.slow_motion = True + + def on_transition_in(self) -> None: + super().on_transition_in() + gnode = bs.getactivity().globalsnode + gnode.tint = (0.5, 0.5, 1) + + act = bs.getactivity().map + shared = SharedObjects.get() + mat = bs.Material() + mat.add_actions(actions=("modify_part_collision", "friction", 0.18)) + + act.node.color = act.bottom.color = (1, 1, 1.2) + act.node.reflection = act.bottom.reflection = "soft" + act.node.materials = [shared.footing_material, mat] + + def _set_meteor_timer(self) -> None: + bs.timer( + (1.0 + 0.2 * random.random()) * self._meteor_time, self._drop_bomb_cluster + ) + + def _drop_bomb_cluster(self) -> None: + defs = self.map.defs + delay = 0.0 + for _i in range(random.randrange(1, 3)): + pos = defs.points["flag_default"] + pos = (pos[0], pos[1] + 0.4, pos[2]) + dropdir = -1.0 if pos[0] > 0 else 1.0 + vel = (random.randrange(-4, 4), 7.0, random.randrange(0, 4)) + bs.timer(delay, babase.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + def _drop_bomb(self, position: Sequence[float], velocity: Sequence[float]) -> None: + Bomb(position=position, velocity=velocity, bomb_type="ice").autoretain() + + def _decrement_meteor_time(self) -> None: + if self._meteor_time < self._min_delay: + return + self._meteor_time = max(0.01, self._meteor_time * 0.9) + if random.choice([0, 0, 1]) == 1: + pos = self.map.defs.points["flag_default"] + self._bots.spawn_bot(PascalBot, pos=pos, spawn_time=2) diff --git a/plugins/minigames/EggGame.py b/plugins/minigames/EggGame.py new file mode 100644 index 0000000..e2d2030 --- /dev/null +++ b/plugins/minigames/EggGame.py @@ -0,0 +1,491 @@ +# Ported by brostos to api 8 +# Tool used to make porting easier.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. + +"""Egg game and support classes.""" +# The Egg Game - throw egg as far as you can +# created in BCS (Bombsquad Consultancy Service) - opensource bombsquad mods for all +# discord.gg/ucyaesh join now and give your contribution +# The Egg game by mr.smoothy +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.powerupbox import PowerupBoxFactory +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.flag import Flag +import math +import random +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + + +class Puck(bs.Actor): + """A lovely giant hockey puck.""" + + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch =None + self.scored = False + self.egg_mesh = bs.getmesh('egg') + self.egg_tex_1 = bs.gettexture('eggTex1') + self.egg_tex_2 = bs.gettexture('eggTex2') + self.egg_tex_3 = bs.gettexture('eggTex3') + self.eggtx=[self.egg_tex_1,self.egg_tex_2,self.egg_tex_3] + regg=random.randrange(0,3) + assert activity is not None + assert isinstance(activity, EggGame) + pmats = [shared.object_material, activity.puck_material] + self.node = bs.newnode('prop', + delegate=self, + attrs={ + 'mesh': self.egg_mesh, + 'color_texture': self.eggtx[regg], + 'body': 'capsule', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.5, + 'body_scale':0.7, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 0.7, 0.26: 0.6}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, bs.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, bs.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch = s_player + else: + super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def on_app_running(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class EggGame(bs.TeamGameActivity[Player, Team]): + """Egg game.""" + + name = 'Epic Egg Game' + description = 'Score some goals.' + available_settings = [ + bs.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + bs.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('40 Seconds', 40), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + bs.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.1), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ] + default_music = bs.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('football') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self.slow_motion = True + self._scoreboard = Scoreboard() + self._cheer_sound = bui.getsound('cheer') + self._chant_sound = bui.getsound('crowdChant') + self._foghorn_sound = bui.getsound('foghorn') + self._swipsound = bui.getsound('swip') + self._whistle_sound = bui.getsound('refWhistle') + self.puck_mesh = bs.getmesh('bomb') + self.puck_tex = bs.gettexture('landMine') + self.puck_scored_tex = bs.gettexture('landMineLit') + self._puck_sound = bui.getsound('metalHit') + self.puck_material = bs.Material() + self._fake_wall_material=bs.Material() + self.HIGHEST=0 + self._fake_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + # self.puck_material.add_actions(conditions=('they_have_material', + # shared.footing_material), + # actions=('impact_sound', + # self._puck_sound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', bs.DieMessage()))) + # self.puck_material.add_actions( + # conditions=('they_have_material',shared.footing_material) + # actions=(('modify_part_collision', 'collide', + # True), ('modify_part_collision', 'physical', True), + # ('call', 'at_connect', self._handle_egg_collision)) + # ) + self._score_region_material = bs.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self.main_ground_material= bs.Material() + + self.main_ground_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_egg_collision))) + + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[bs.NodeActor]] = None + self._puck: Optional[Puck] = None + self._pucks=[] + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + return "Throw Egg as far u can" + + def get_instance_description_short(self) -> Union[str, Sequence]: + return "Throw Egg as far u can" + + def on_begin(self) -> None: + super().on_begin() + if self._time_limit==0.0: + self._time_limit=60 + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + self._puck_spawn_pos = self.map.get_flag_position(None) + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + pos=(11.88630542755127, 0.3009839951992035, 1.33331298828125) + # mat=bs.Material() + # mat.add_actions( + + # actions=( ('modify_part_collision','physical',True), + # ('modify_part_collision','collide',True)) + # ) + # self._score_regions.append( + # bs.NodeActor( + # bs.newnode('region', + # attrs={ + # 'position': pos, + # 'scale': (2,3,5), + # 'type': 'box', + # 'materials': [self._score_region_material] + # }))) + # pos=(-11.88630542755127, 0.3009839951992035, 1.33331298828125) + # self._score_regions.append( + # bs.NodeActor( + # bs.newnode('region', + # attrs={ + # 'position': pos, + # 'scale': (2,3,5), + # 'type': 'box', + # 'materials': [self._score_region_material] + # }))) + self._score_regions.append( + bs.NodeActor( + bs.newnode('region', + attrs={ + 'position': (-9.21,defs.boxes['goal2'][0:3][1],defs.boxes['goal2'][0:3][2]), + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': (self._fake_wall_material, ) + }))) + pos=(0,0.1,-5) + self.main_ground=bs.newnode('region',attrs={'position': pos,'scale': (25,0.001,22),'type': 'box','materials': [self.main_ground_material]}) + self._update_scoreboard() + self._chant_sound.play() + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = bs.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except bs.NotFoundError: + return + + puck.last_players_to_touch = player + + def _kill_puck(self) -> None: + self._puck = None + def _handle_egg_collision(self) -> None: + + no=bs.getcollision().opposingnode + pos=no.position + egg=no.getdelegate(Puck) + source_player=egg.last_players_to_touch + if source_player==None or pos[0]< -8 or not source_player.node.exists() : + return + + + try: + col=source_player.team.color + self.flagg=Flag(pos,touchable=False,color=col).autoretain() + self.flagg.is_area_of_interest=True + player_pos=source_player.node.position + + distance = math.sqrt( pow(player_pos[0]-pos[0],2) + pow(player_pos[2]-pos[2],2)) + + + dis_mark=bs.newnode('text', + + attrs={ + 'text':str(round(distance,2))+"m", + 'in_world':True, + 'scale':0.02, + 'h_align':'center', + 'position':(pos[0],1.6,pos[2]), + 'color':col + }) + bs.animate(dis_mark,'scale',{ + 0.0:0, 0.5:0.01 + }) + if distance > self.HIGHEST: + self.HIGHEST=distance + self.stats.player_scored( + source_player, + 10, + big_message=False) + + no.delete() + bs.timer(2,self._spawn_puck) + source_player.team.score=int(distance) + + except(): + pass + def spawn_player(self, player: Player) -> bs.Actor: + + + zoo=random.randrange(-4,5) + pos=(-11.204887390136719, 0.2998693287372589, zoo) + spaz = self.spawn_player_spaz( + player, position=pos, angle=90 ) + assert spaz.node + + # Prevent controlling of characters before the start of the race. + + return spaz + def _handle_score(self) -> None: + """A point has been scored.""" + + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = bs.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(bs.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 20, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + self._foghorn_sound.play() + self._cheer_sound.play() + + # self._puck.scored = True + + # Change puck texture to something cool + # self._puck.node.color_texture = self.puck_scored_tex + # Kill the puck (it'll respawn itself shortly). + bs.timer(1.0, self._kill_puck) + + # light = bs.newnode('light', + # attrs={ + # 'position': bs.getcollision().position, + # 'height_attenuated': False, + # 'color': (1, 0, 0) + # }) + # bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + # bs.timer(1.0, light.delete) + + bs.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + # for team in self.teams: + # self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, bs.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + bs.timer(3.0, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + # light = bs.newnode('light', + # attrs={ + # 'position': self._puck_spawn_pos, + # 'height_attenuated': False, + # 'color': (1, 0, 0) + # }) + # bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + # bs.timer(1.0, light.delete) + pass + def _spawn_puck(self) -> None: + # self._swipsound.play() + # self._whistle_sound.play() + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + zoo=random.randrange(-5,6) + pos=(-11.204887390136719, 0.2998693287372589, zoo) + self._pucks.append (Puck(position=pos)) diff --git a/plugins/minigames/HYPER_RACE.py b/plugins/minigames/HYPER_RACE.py new file mode 100644 index 0000000..e09478c --- /dev/null +++ b/plugins/minigames/HYPER_RACE.py @@ -0,0 +1,1239 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +from dataclasses import dataclass + +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1 import _map +from bascenev1lib.actor.bomb import Bomb, Blast, BombFactory +from bascenev1lib.actor.powerupbox import PowerupBox +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict, + Union) + from bascenev1lib.actor.onscreentimer import OnScreenTimer + + +class ThePadDefs: + points = {} + boxes = {} + points['race_mine1'] = (0, 5, 12) + points['race_point1'] = (0.2, 5, 2.86308) + (0.507, 4.673, 1.1) + points['race_point2'] = (6.9301, 5.04988, 2.82066) + (0.911, 4.577, 1.073) + points['race_point3'] = (6.98857, 4.5011, -8.88703) + (1.083, 4.673, 1.076) + points['race_point4'] = (-6.4441, 4.5011, -8.88703) + (1.083, 4.673, 1.076) + points['race_point5'] = (-6.31128, 4.5011, 2.82669) + (0.894, 4.673, 0.941) + boxes['area_of_interest_bounds'] = ( + 0.3544110667, 4.493562578, -2.518391331) + ( + 0.0, 0.0, 0.0) + (16.64754831, 8.06138989, 18.5029888) + points['ffa_spawn1'] = (-0, 5, 2.5) + points['flag1'] = (-7.026110145, 4.308759233, -6.302807727) + points['flag2'] = (7.632557137, 4.366002373, -6.287969342) + points['flagDefault'] = (0.4611826686, 4.382076338, 3.680881802) + boxes['map_bounds'] = (0.2608783669, 4.899663734, -3.543675157) + ( + 0.0, 0.0, 0.0) + (29.23565494, 14.19991443, 29.92689344) + points['powerup_spawn1'] = (-4.166594349, 5.281834349, -6.427493781) + points['powerup_spawn2'] = (4.426873526, 5.342460464, -6.329745237) + points['powerup_spawn3'] = (-4.201686731, 5.123385835, 0.4400721376) + points['powerup_spawn4'] = (4.758924722, 5.123385835, 0.3494054559) + points['shadow_lower_bottom'] = (-0.2912522507, 2.020798381, 5.341226521) + points['shadow_lower_top'] = (-0.2912522507, 3.206066063, 5.341226521) + points['shadow_upper_bottom'] = (-0.2912522507, 6.062361813, 5.341226521) + points['shadow_upper_top'] = (-0.2912522507, 9.827201965, 5.341226521) + points['spawn1'] = (-0, 5, 2.5) + points['tnt1'] = (0.4599593402, 4.044276501, -6.573537395) + + +class ThePadMapb(bs.Map): + defs = ThePadDefs() + name = 'Racing' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['hyper'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'thePadPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'mesh': bs.getmesh('thePadLevel'), + 'bottom_mesh': bs.getmesh('thePadLevelBottom'), + 'collision_mesh': bs.getcollisionmesh('thePadLevelCollide'), + 'tex': bs.gettexture('thePadLevelColor'), + 'bgtex': bs.gettexture('black'), + 'bgmesh': bs.getmesh('thePadBG'), + 'railing_collision_mesh': bs.getcollisionmesh('thePadLevelBumper'), + 'vr_fill_mound_mesh': bs.getmesh('thePadVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound') + } + # fixme should chop this into vr/non-vr sections for efficiency + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = bs.newnode( + 'terrain', + delegate=self, + attrs={ + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.bottom = bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['bottom_mesh'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.background = bs.newnode( + 'terrain', + attrs={ + 'mesh': self.preloaddata['bgmesh'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + self.railing = bs.newnode( + 'terrain', + attrs={ + 'collision_mesh': self.preloaddata['railing_collision_mesh'], + 'materials': [shared.railing_material], + 'bumper': True + }) + bs.newnode('terrain', + attrs={ + 'mesh': self.preloaddata['vr_fill_mound_mesh'], + 'lighting': False, + 'vr_only': True, + 'color': (0.56, 0.55, 0.47), + 'background': True, + 'color_texture': self.preloaddata['vr_fill_mound_tex'] + }) + gnode = bs.getactivity().globalsnode + gnode.tint = (1.1, 1.1, 1.0) + gnode.ambient_color = (1.1, 1.1, 1.0) + gnode.vignette_outer = (0.7, 0.65, 0.75) + gnode.vignette_inner = (0.95, 0.95, 0.93) + + +# ba_meta export plugin +class NewMap(babase.Plugin): + """My first ballistica plugin!""" + + def on_app_running(self) -> None: + _map.register_map(ThePadMapb) + + +class NewBlast(Blast): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + blast_radius: float = 2.0, + blast_type: str = 'normal', + source_player: bs.Player = None, + hit_type: str = 'explosion', + hit_subtype: str = 'normal'): + bs.Actor.__init__(self) + + shared = SharedObjects.get() + factory = BombFactory.get() + + self.blast_type = blast_type + self._source_player = source_player + self.hit_type = hit_type + self.hit_subtype = hit_subtype + self.radius = blast_radius + + # Set our position a bit lower so we throw more things upward. + rmats = (factory.blast_material, shared.attack_material) + self.node = bs.newnode( + 'region', + delegate=self, + attrs={ + 'position': (position[0], position[1] - 0.1, position[2]), + 'scale': (self.radius, self.radius, self.radius), + 'type': 'sphere', + 'materials': rmats + }, + ) + + bs.timer(0.05, self.node.delete) + + # Throw in an explosion and flash. + evel = (velocity[0], max(-1.0, velocity[1]), velocity[2]) + explosion = bs.newnode('explosion', + attrs={ + 'position': position, + 'velocity': evel, + 'radius': self.radius, + 'big': (self.blast_type == 'tnt') + }) + if self.blast_type == 'ice': + explosion.color = (0, 0.05, 0.4) + + bs.timer(1.0, explosion.delete) + + if self.blast_type != 'ice': + bs.emitfx(position=position, + velocity=velocity, + count=int(1.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='thin_smoke') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='ice' if self.blast_type == 'ice' else 'smoke') + bs.emitfx(position=position, + emit_type='distortion', + spread=1.0 if self.blast_type == 'tnt' else 2.0) + + # And emit some shrapnel. + if self.blast_type == 'ice': + + def emit() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=30, + spread=2.0, + scale=0.4, + chunk_type='ice', + emit_type='stickers') + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + elif self.blast_type == 'sticky': + + def emit() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + spread=0.7, + chunk_type='slime') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.5, + spread=0.7, + chunk_type='slime') + bs.emitfx(position=position, + velocity=velocity, + count=15, + scale=0.6, + chunk_type='slime', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=20, + scale=0.7, + chunk_type='spark', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=int(6.0 + random.random() * 12), + scale=0.8, + spread=1.5, + chunk_type='spark') + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + elif self.blast_type == 'impact': + + def emit() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.8, + chunk_type='metal') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.4, + chunk_type='metal') + bs.emitfx(position=position, + velocity=velocity, + count=20, + scale=0.7, + chunk_type='spark', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=int(8.0 + random.random() * 15), + scale=0.8, + spread=1.5, + chunk_type='spark') + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + else: # Regular or land mine bomb shrapnel. + + def emit() -> None: + if self.blast_type != 'tnt': + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + chunk_type='rock') + bs.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.5, + chunk_type='rock') + bs.emitfx(position=position, + velocity=velocity, + count=30, + scale=1.0 if self.blast_type == 'tnt' else 0.7, + chunk_type='spark', + emit_type='stickers') + bs.emitfx(position=position, + velocity=velocity, + count=int(18.0 + random.random() * 20), + scale=1.0 if self.blast_type == 'tnt' else 0.8, + spread=1.5, + chunk_type='spark') + + # TNT throws splintery chunks. + if self.blast_type == 'tnt': + + def emit_splinters() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(20.0 + random.random() * 25), + scale=0.8, + spread=1.0, + chunk_type='splinter') + + bs.timer(0.01, emit_splinters) + + # Every now and then do a sparky one. + if self.blast_type == 'tnt' or random.random() < 0.1: + + def emit_extra_sparks() -> None: + bs.emitfx(position=position, + velocity=velocity, + count=int(10.0 + random.random() * 20), + scale=0.8, + spread=1.5, + chunk_type='spark') + + bs.timer(0.02, emit_extra_sparks) + + # It looks better if we delay a bit. + bs.timer(0.05, emit) + + lcolor = ((0.6, 0.6, 1.0) if self.blast_type == 'ice' else + (1, 0.3, 0.1)) + light = bs.newnode('light', + attrs={ + 'position': position, + 'volume_intensity_scale': 10.0, + 'color': lcolor + }) + + scl = random.uniform(0.6, 0.9) + scorch_radius = light_radius = self.radius + if self.blast_type == 'tnt': + light_radius *= 1.4 + scorch_radius *= 1.15 + scl *= 3.0 + + iscale = 1.6 + bs.animate( + light, 'intensity', { + 0: 2.0 * iscale, + scl * 0.02: 0.1 * iscale, + scl * 0.025: 0.2 * iscale, + scl * 0.05: 17.0 * iscale, + scl * 0.06: 5.0 * iscale, + scl * 0.08: 4.0 * iscale, + scl * 0.2: 0.6 * iscale, + scl * 2.0: 0.00 * iscale, + scl * 3.0: 0.0 + }) + bs.animate( + light, 'radius', { + 0: light_radius * 0.2, + scl * 0.05: light_radius * 0.55, + scl * 0.1: light_radius * 0.3, + scl * 0.3: light_radius * 0.15, + scl * 1.0: light_radius * 0.05 + }) + bs.timer(scl * 3.0, light.delete) + + # Make a scorch that fades over time. + scorch = bs.newnode('scorch', + attrs={ + 'position': position, + 'size': scorch_radius * 0.5, + 'big': (self.blast_type == 'tnt') + }) + if self.blast_type == 'ice': + scorch.color = (1, 1, 1.5) + + bs.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) + bs.timer(13.0, scorch.delete) + + if self.blast_type == 'ice': + factory.hiss_sound.play(position=light.position) + + lpos = light.position + factory.random_explode_sound().play(position=lpos) + factory.debris_fall_sound.play(position=lpos) + + bs.camerashake(0.0) + + # TNT is more epic. + if self.blast_type == 'tnt': + factory.random_explode_sound().play(position=lpos) + + def _extra_boom() -> None: + factory.random_explode_sound().play(position=lpos) + + bs.timer(0.25, _extra_boom) + + def _extra_debris_sound() -> None: + factory.debris_fall_sound.play(position=lpos) + factory.wood_debris_fall_sound.play(position=lpos) + + bs.timer(0.4, _extra_debris_sound) + + +class NewBomb(Bomb): + + def explode(self) -> None: + """Blows up the bomb if it has not yet done so.""" + if self._exploded: + return + self._exploded = True + if self.node: + blast = NewBlast(position=self.node.position, + velocity=self.node.velocity, + blast_radius=self.blast_radius, + blast_type=self.bomb_type, + source_player=babase.existing(self._source_player), + hit_type=self.hit_type, + hit_subtype=self.hit_subtype).autoretain() + for callback in self._explode_callbacks: + callback(self, blast) + + # We blew up so we need to go away. + # NOTE TO SELF: do we actually need this delay? + bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage())) + + +class TNT(bs.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + tnt_scale: float = 1.0, + teleport: bool = True): + super().__init__() + self.position = position + self.teleport = teleport + + self._no_collide_material = bs.Material() + self._no_collide_material.add_actions( + actions=('modify_part_collision', 'collide', False), + ) + self._collide_material = bs.Material() + self._collide_material.add_actions( + actions=('modify_part_collision', 'collide', True), + ) + + if teleport: + collide = self._collide_material + else: + collide = self._no_collide_material + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'mesh': bs.getmesh('tnt'), + 'color_texture': bs.gettexture('tnt'), + 'body': 'crate', + 'mesh_scale': tnt_scale, + 'body_scale': tnt_scale, + 'density': 2.0, + 'gravity_scale': 2.0, + 'materials': [collide] + } + ) + if not teleport: + bs.timer(0.1, self._collide) + + def _collide(self) -> None: + self.node.materials += (self._collide_material,) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.OutOfBoundsMessage): + if self.teleport: + self.node.position = self.position + self.node.velocity = (0, 0, 0) + else: + self.node.delete() + else: + super().handlemessage(msg) + + +class RaceRegion(bs.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 = bs.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] + }) + + +# MINIGAME +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.distance_txt: Optional[bs.Node] = None + self.last_region = 0 + self.lap = 0 + self.distance = 0.0 + self.finished = False + self.rank: Optional[int] = None + + +class Team(bs.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 bascenev1.GameActivity +class RaceGame(bs.TeamGameActivity[Player, Team]): + """Game of racing around a track.""" + + name = 'Hyper Race' + description = 'Creado Por Cebolla!!' + scoreconfig = bs.ScoreConfig(label='Time', + lower_is_better=True, + scoretype=bs.ScoreType.MILLISECONDS) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting('Laps', min_value=1, default=3, increment=1), + bs.IntChoiceSetting( + 'Time Limit', + default=0, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + ), + bs.BoolSetting('Epic Mode', default=False), + ] + + # We have some specific settings in teams mode. + if issubclass(sessiontype, bs.DualTeamSession): + settings.append( + bs.BoolSetting('Entire Team Must Finish', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.MultiTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return bs.app.classic.getmaps('hyper') + + def __init__(self, settings: dict): + self._race_started = False + super().__init__(settings) + self.factory = factory = BombFactory.get() + self.shared = shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._score_sound = bs.getsound('score') + self._swipsound = bs.getsound('swip') + self._last_team_time: Optional[float] = None + self._front_race_region: Optional[int] = None + self._nub_tex = bs.gettexture('nub') + self._beep_1_sound = bs.getsound('raceBeep1') + self._beep_2_sound = bs.getsound('raceBeep2') + self.race_region_material: Optional[bs.Material] = None + self._regions: List[RaceRegion] = [] + self._team_finish_pts: Optional[int] = None + self._time_text: Optional[bs.Actor] = None + self._timer: Optional[OnScreenTimer] = None + self._scoreboard_timer: Optional[bs.Timer] = None + self._player_order_update_timer: Optional[bs.Timer] = None + self._start_lights: Optional[List[bs.Node]] = 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._epic_mode = bool(settings['Epic Mode']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC_RACE + if self._epic_mode else bs.MusicType.RACE) + + self._safe_region_material = bs.Material() + self._safe_region_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True)) + ) + + def get_instance_description(self) -> Union[str, Sequence]: + if (isinstance(self.session, bs.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 = bs.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))) + + bs.newnode( + 'region', + attrs={ + 'position': (0.3, 4.044276501, -2.9), + 'scale': (11.7, 15, 9.5), + 'type': 'box', + 'materials': [self._safe_region_material] + } + ) + + def _flash_player(self, player: Player, scale: float) -> None: + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + pos = player.actor.node.position + light = bs.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + bs.timer(0.5, light.delete) + bs.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 = bs.getcollision() + try: + region = collision.sourcenode.getdelegate(RaceRegion, True) + spaz = collision.opposingnode.getdelegate(PlayerSpaz,True) + except bs.NotFoundError: + return + + if not spaz.is_alive(): + return + + try: + player = spaz.getplayer(Player, True) + except bs.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(bs.DieMessage()) + bs.broadcastmessage(babase.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, bs.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, bs.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( + bs.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: + self._score_sound.play() + player.team.finished = True + assert self._timer is not None + elapsed = bs.time() - self._timer.getstarttime() + self._last_team_time = player.team.time = elapsed + self._check_end_game() + + # Team has yet to finish. + else: + self._swipsound.play() + + # They've just finished a lap but not the race. + else: + self._swipsound.play() + self._flash_player(player, 0.3) + + # Print their lap number over their head. + try: + assert isinstance(player.actor, PlayerSpaz) + mathnode = bs.newnode('math', + owner=player.actor.node, + attrs={ + 'input1': (0, 1.9, 0), + 'operation': 'add' + }) + player.actor.node.connectattr( + 'torso_position', mathnode, 'input2') + tstr = babase.Lstr(resource='lapNumberText', + subs=[('${CURRENT}', + str(player.lap + 1)), + ('${TOTAL}', str(self._laps)) + ]) + txtnode = bs.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') + bs.animate(txtnode, 'scale', { + 0.0: 0, + 0.2: 0.019, + 2.0: 0.019, + 2.2: 0 + }) + bs.timer(2.3, mathnode.delete) + except Exception: + babase.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, bs.DualTeamSession) + and self._entire_team_must_finish): + bs.broadcastmessage(babase.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 + bs.getsound('boo').play() + for otherplayer in player.team.players: + otherplayer.lap = 0 + otherplayer.finished = True + try: + if otherplayer.actor is not None: + otherplayer.actor.handlemessage(bs.DieMessage()) + except Exception: + babase.print_exception('Error sending DieMessage.') + + # Defer so team/player lists will be updated. + babase.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, bs.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 bascenev1lib.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 + + # Throw a timer up on-screen. + self._time_text = bs.NodeActor( + bs.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() + + self._scoreboard_timer = bs.Timer(0.25, + self._update_scoreboard, + repeat=True) + self._player_order_update_timer = bs.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 + + bs.timer(lstart, self._do_light_1) + bs.timer(lstart + inc, self._do_light_2) + bs.timer(lstart + 2 * inc, self._do_light_3) + bs.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = bs.newnode('image', + attrs={ + 'texture': bs.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + bs.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 + }) + bs.timer(13.0 * t_scale, lnub.delete) + self._start_lights.append(lnub) + + self._obstacles() + + pts = self.map.get_def_points('race_point') + for rpt in pts: + bs.newnode( + 'locator', + attrs={ + 'shape': 'circle', + 'position': (rpt[0], 4.382076338, rpt[2]), + 'size': (rpt[3] * 2.0, 0, rpt[5] * 2.0), + 'color': (0, 1, 0), + 'opacity': 1.0, + 'draw_beauty': False, + 'additive': True + } + ) + + def _obstacles(self) -> None: + 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) + + self._tnt((1.5, 5, 2.3), (0, 0, 0), 1.0) + self._tnt((1.5, 5, 3.3), (0, 0, 0), 1.0) + + self._tnt((3.5, 5, 2.3), (0, 0, 0), 1.0) + self._tnt((3.5, 5, 3.3), (0, 0, 0), 1.0) + + self._tnt((5.5, 5, 2.3), (0, 0, 0), 1.0) + self._tnt((5.5, 5, 3.3), (0, 0, 0), 1.0) + + self._tnt((-6, 5, -7), (0, 0, 0), 1.3) + self._tnt((-7, 5, -5), (0, 0, 0), 1.3) + self._tnt((-6, 5, -3), (0, 0, 0), 1.3) + self._tnt((-7, 5, -1), (0, 0, 0), 1.3) + self._tnt((-6, 5, 1), (0, 0, 0), 1.3) + + bs.timer(0.1, bs.WeakCall(self._tnt, (-3.2, 5, 1), + (0, 0, 0), 1.0, (0, 20, 60)), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (6.8, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (7.6, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (-5, 5, 0), (0, 0, 0), 1.0, 1.0, (0, 20, 3)), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'impact', + (-1.5, 5, 0), (0, 0, 0), 1.0, 1.0, (0, 20, 3)), repeat=True) + + + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-1, 5, -8), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-1, 5, -9), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-1, 5, -10), (0, 10, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-4.6, 5, -8), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-4.6, 5, -9), (0, 10, 0), 1.0, 1.0), repeat=True) + bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky', + (-4.6, 5, -10), (0, 10, 0), 1.0, 1.0), repeat=True) + + bs.timer(1.6, bs.WeakCall( + self._powerup, (2, 5, -5), 'curse', (0, 20, -3)), repeat=True) + bs.timer(1.6, bs.WeakCall( + self._powerup, (4, 5, -5), 'curse', (0, 20, -3)), repeat=True) + + def _tnt(self, + position: float, + velocity: float, + tnt_scale: float, + extra_acceleration: float = None) -> None: + if extra_acceleration: + TNT(position, velocity, tnt_scale, False).autoretain( + ).node.extra_acceleration = extra_acceleration + else: + TNT(position, velocity, tnt_scale).autoretain() + + def _bomb(self, + type: str, + position: float, + velocity: float, + mesh_scale: float, + body_scale: float, + extra_acceleration: float = None) -> None: + if extra_acceleration: + NewBomb(position=position, + velocity=velocity, + bomb_type=type).autoretain( + ).node.extra_acceleration = extra_acceleration + else: + NewBomb(position=position, + velocity=velocity, + bomb_type=type).autoretain() + + def _powerup(self, + position: float, + poweruptype: str, + extra_acceleration: float = None) -> None: + if extra_acceleration: + PowerupBox(position=position, + poweruptype=poweruptype).autoretain( + ).node.extra_acceleration = extra_acceleration + else: + PowerupBox(position=position, poweruptype=poweruptype).autoretain() + + def _do_light_1(self) -> None: + assert self._start_lights is not None + self._start_lights[0].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + self._beep_1_sound.play() + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + self._beep_1_sound.play() + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + self._beep_2_sound.play() + 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: + babase.print_exception('Error in race player connects.') + assert self._timer is not None + self._timer.start() + + self._race_started = True + + def _update_player_order(self) -> None: + + # Calc all player distances. + for player in self.players: + pos: Optional[babase.Vec3] + try: + pos = player.position + except bs.NotFoundError: + pos = None + if pos is not None: + r_index = player.last_region + rg1 = self._regions[r_index] + r1pt = babase.Vec3(rg1.pos[:3]) + rg2 = self._regions[0] if r_index == len( + self._regions) - 1 else self._regions[r_index + 1] + r2pt = babase.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_player(self, player: Player) -> bs.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 = bs.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + spaz.node.connectattr('torso_position', mathnode, 'input2') + + distance_txt = bs.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') + return spaz + + 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, bs.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, bs.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 = bs.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, + bs.DualTeamSession)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.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/SnowBallFight.py b/plugins/minigames/SnowBallFight.py new file mode 100644 index 0000000..29afe38 --- /dev/null +++ b/plugins/minigames/SnowBallFight.py @@ -0,0 +1,643 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor.bomb import Blast +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.spaz import PunchHitMessage +from bascenev1lib.actor.playerspaz import PlayerSpaz +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor.spazfactory import SpazFactory + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = bs.app.lang.language + +if lang == 'Spanish': + name = 'Guerra de Nieve' + snowball_rate = 'Intervalo de Ataque' + snowball_slowest = 'Más Lento' + snowball_slow = 'Lento' + snowball_fast = 'Rápido' + snowball_lagcity = 'Más Rápido' + snowball_scale = 'Tamaño de Bola de Nieve' + snowball_smallest = 'Más Pequeño' + snowball_small = 'Pequeño' + snowball_big = 'Grande' + snowball_biggest = 'Más Grande' + snowball_insane = 'Insano' + snowball_melt = 'Derretir Bola de Nieve' + snowball_bust = 'Rebotar Bola de Nieve' + snowball_explode = 'Explotar al Impactar' + snowball_snow = 'Modo Nieve' +else: + name = 'Snowball Fight' + snowball_rate = 'Snowball Rate' + snowball_slowest = 'Slowest' + snowball_slow = 'Slow' + snowball_fast = 'Fast' + snowball_lagcity = 'Lag City' + snowball_scale = 'Snowball Scale' + snowball_smallest = 'Smallest' + snowball_small = 'Small' + snowball_big = 'Big' + snowball_biggest = 'Biggest' + snowball_insane = 'Insane' + snowball_melt = 'Snowballs Melt' + snowball_bust = 'Snowballs Bust' + snowball_explode = 'Snowballs Explode' + snowball_snow = 'Snow Mode' + + +class Snowball(bs.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + blast_radius: float = 0.7, + bomb_scale: float = 0.8, + source_player: bs.Player | None = None, + owner: bs.Node | None = None, + melt: bool = True, + bounce: bool = True, + explode: bool = False): + super().__init__() + shared = SharedObjects.get() + self._exploded = False + self.scale = bomb_scale + self.blast_radius = blast_radius + self._source_player = source_player + self.owner = owner + self._hit_nodes = set() + self.snowball_melt = melt + self.snowball_bounce = bounce + self.snowball_explode = explode + self.radius = bomb_scale * 0.1 + if bomb_scale <= 1.0: + shadow_size = 0.6 + elif bomb_scale <= 2.0: + shadow_size = 0.4 + elif bomb_scale <= 3.0: + shadow_size = 0.2 + else: + shadow_size = 0.1 + + self.snowball_material = bs.Material() + self.snowball_material.add_actions( + conditions=( + ( + ('we_are_younger_than', 5), + 'or', + ('they_are_younger_than', 100), + ), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + + self.snowball_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=('modify_part_collision', 'use_node_collide', False), + ) + + self.snowball_material.add_actions(actions=('modify_part_collision', + 'friction', 0.3)) + + self.snowball_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('modify_part_collision', 'physical', False), + ('call', 'at_connect', self.hit))) + + self.snowball_material.add_actions( + conditions=(('they_dont_have_material', shared.player_material), + 'and', + ('they_have_material', shared.object_material), + 'or', + ('they_have_material', shared.footing_material)), + actions=('call', 'at_connect', self.bounce)) + + self.node = bs.newnode( + 'prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'body': 'sphere', + 'body_scale': self.scale, + 'mesh': bs.getmesh('frostyPelvis'), + 'shadow_size': shadow_size, + 'color_texture': bs.gettexture('bunnyColor'), + 'reflection': 'soft', + 'reflection_scale': [0.15], + 'density': 1.0, + 'materials': [self.snowball_material] + }) + self.light = bs.newnode( + 'light', + owner=self.node, + attrs={ + 'color': (0.6, 0.6, 1.0), + 'intensity': 0.8, + 'radius': self.radius + }) + self.node.connectattr('position', self.light, 'position') + bs.animate(self.node, 'mesh_scale', { + 0: 0, + 0.2: 1.3 * self.scale, + 0.26: self.scale + }) + bs.animate(self.light, 'radius', { + 0: 0, + 0.2: 1.3 * self.radius, + 0.26: self.radius + }) + if self.snowball_melt: + bs.timer(1.5, bs.WeakCall(self._disappear)) + + def hit(self) -> None: + if not self.node: + return + if self._exploded: + return + if self.snowball_explode: + self._exploded = True + self.do_explode() + bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage())) + else: + self.do_hit() + + def do_hit(self) -> None: + v = self.node.velocity + if babase.Vec3(*v).length() > 5.0: + node = bs.getcollision().opposingnode + if node is not None and node and not ( + node in self._hit_nodes): + t = self.node.position + hitdir = self.node.velocity + self._hit_nodes.add(node) + node.handlemessage( + bs.HitMessage( + pos=t, + velocity=v, + magnitude=babase.Vec3(*v).length()*0.5, + velocity_magnitude=babase.Vec3(*v).length()*0.5, + radius=0, + srcnode=self.node, + source_player=self._source_player, + force_direction=hitdir, + hit_type='snoBall', + hit_subtype='default')) + + if not self.snowball_bounce: + bs.timer(0.05, bs.WeakCall(self.do_bounce)) + + def do_explode(self) -> None: + Blast(position=self.node.position, + velocity=self.node.velocity, + blast_radius=self.blast_radius, + source_player=babase.existing(self._source_player), + blast_type='impact', + hit_subtype='explode').autoretain() + + def bounce(self) -> None: + if not self.node: + return + if self._exploded: + return + if not self.snowball_bounce: + vel = self.node.velocity + bs.timer(0.01, bs.WeakCall(self.calc_bounce, vel)) + else: + return + + def calc_bounce(self, vel) -> None: + if not self.node: + return + ospd = babase.Vec3(*vel).length() + dot = sum(x*y for x, y in zip(vel, self.node.velocity)) + if ospd*ospd - dot > 50.0: + bs.timer(0.05, bs.WeakCall(self.do_bounce)) + + def do_bounce(self) -> None: + if not self.node: + return + if not self._exploded: + self.do_effect() + + def do_effect(self) -> None: + self._exploded = True + bs.emitfx(position=self.node.position, + velocity=[v*0.1 for v in self.node.velocity], + count=10, + spread=0.1, + scale=0.4, + chunk_type='ice') + sound = bs.getsound('impactMedium') + sound.play(1.0, position=self.node.position) + scl = self.node.mesh_scale + bs.animate(self.node, 'mesh_scale', { + 0.0: scl*1.0, + 0.02: scl*0.5, + 0.05: 0.0 + }) + lr = self.light.radius + bs.animate(self.light, 'radius', { + 0.0: lr*1.0, + 0.02: lr*0.5, + 0.05: 0.0 + }) + bs.timer(0.08, + bs.WeakCall(self.handlemessage, bs.DieMessage())) + + def _disappear(self) -> None: + self._exploded = True + if self.node: + scl = self.node.mesh_scale + bs.animate(self.node, 'mesh_scale', { + 0.0: scl*1.0, + 0.3: scl*0.5, + 0.5: 0.0 + }) + lr = self.light.radius + bs.animate(self.light, 'radius', { + 0.0: lr*1.0, + 0.3: lr*0.5, + 0.5: 0.0 + }) + bs.timer(0.55, + bs.WeakCall(self.handlemessage, bs.DieMessage())) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + if self.node: + self.node.delete() + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) + else: + super().handlemessage(msg) + + +class NewPlayerSpaz(PlayerSpaz): + + def __init__(self, *args: Any, **kwds: Any): + super().__init__(*args, **kwds) + self.snowball_scale = 1.0 + self.snowball_melt = True + self.snowball_bounce = True + self.snowball_explode = False + + def on_punch_press(self) -> None: + if not self.node or self.frozen or self.node.knockout > 0.0: + return + t_ms = bs.time() * 1000 + assert isinstance(t_ms, int) + if t_ms - self.last_punch_time_ms >= self._punch_cooldown: + if self.punch_callback is not None: + self.punch_callback(self) + + # snowball + pos = self.node.position + p1 = self.node.position_center + p2 = self.node.position_forward + direction = [p1[0]-p2[0],p2[1]-p1[1],p1[2]-p2[2]] + direction[1] = 0.03 + mag = 20.0/babase.Vec3(*direction).length() + vel = [v * mag for v in direction] + Snowball(position=(pos[0], pos[1] + 0.1, pos[2]), + velocity=vel, + blast_radius=self.blast_radius, + bomb_scale=self.snowball_scale, + source_player=self.source_player, + owner=self.node, + melt=self.snowball_melt, + bounce=self.snowball_bounce, + explode=self.snowball_explode).autoretain() + + self._punched_nodes = set() # Reset this. + self.last_punch_time_ms = t_ms + self.node.punch_pressed = True + if not self.node.hold_node: + bs.timer( + 0.1, + bs.WeakCall(self._safe_play_sound, + SpazFactory.get().swish_sound, 0.8)) + self._turbo_filter_add_press('punch') + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, PunchHitMessage): + pass + else: + return super().handlemessage(msg) + return None + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export bascenev1.GameActivity +class SnowballFightGame(bs.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = name + description = 'Kill a set number of enemies to win.' + + # 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( + 'Kills to Win Per Player', + min_value=1, + default=5, + 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.IntChoiceSetting( + snowball_rate, + choices=[ + (snowball_slowest, 500), + (snowball_slow, 400), + ('Normal', 300), + (snowball_fast, 200), + (snowball_lagcity, 100), + ], + default=300, + ), + bs.FloatChoiceSetting( + snowball_scale, + choices=[ + (snowball_smallest, 0.4), + (snowball_small, 0.6), + ('Normal', 0.8), + (snowball_big, 1.4), + (snowball_biggest, 3.0), + (snowball_insane, 6.0), + ], + default=0.8, + ), + bs.BoolSetting(snowball_melt, default=True), + bs.BoolSetting(snowball_bust, default=True), + bs.BoolSetting(snowball_explode, default=False), + bs.BoolSetting(snowball_snow, default=True), + bs.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, bs.FreeForAllSession): + settings.append( + bs.BoolSetting('Allow Negative Scores', 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('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: int | None = None + self._dingsound = bs.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)) + self._snowball_rate = int(settings[snowball_rate]) + self._snowball_scale = float(settings[snowball_scale]) + self._snowball_melt = bool(settings[snowball_melt]) + self._snowball_bounce = bool(settings[snowball_bust]) + self._snowball_explode = bool(settings[snowball_explode]) + self._snow_mode = bool(settings[snowball_snow]) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC if self._epic_mode else + bs.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> str | Sequence: + return 'Crush ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> str | Sequence: + return 'kill ${ARG1} enemies', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_transition_in(self) -> None: + super().on_transition_in() + if self._snow_mode: + gnode = bs.getactivity().globalsnode + gnode.tint = (0.8, 0.8, 1.3) + bs.timer(0.02, self.emit_snowball, repeat=True) + + 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() + + def emit_snowball(self) -> None: + pos = (-10 + (random.random() * 30), 15, + -10 + (random.random() * 30)) + vel = ((-5.0 + random.random() * 30.0) * (-1.0 if pos[0] > 0 else 1.0), + -50.0, (-5.0 + random.random() * 30.0) * ( + -1.0 if pos[0] > 0 else 1.0)) + bs.emitfx(position=pos, + velocity=vel, + count=10, + scale=1.0 + random.random(), + spread=0.0, + chunk_type='spark') + + def spawn_player_spaz(self, + player: Player, + position: Sequence[float] = (0, 0, 0), + angle: float | None = None) -> PlayerSpaz: + from babase import _math + from bascenev1._gameutils import animate + from bascenev1._coopsession import CoopSession + + if isinstance(self.session, bs.DualTeamSession): + position = self.map.get_start_position(player.team.id) + else: + # otherwise do free-for-all spawn locations + position = self.map.get_ffa_start_position(self.players) + + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = babase.safecolor(color, target_intensity=0.75) + + spaz = NewPlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + # If this is co-op and we're on Courtyard or Runaround, add the + # material that allows us to collide with the player-walls. + # FIXME: Need to generalize this. + if isinstance(self.session, CoopSession) and self.map.getname() in [ + 'Courtyard', 'Tower D' + ]: + mat = self.map.preloaddata['collide_with_wall_material'] + assert isinstance(spaz.node.materials, tuple) + assert isinstance(spaz.node.roller_materials, tuple) + spaz.node.materials += (mat, ) + spaz.node.roller_materials += (mat, ) + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player( + enable_pickup=False, enable_bomb=False) + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + bs.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + self._spawn_sound.play(1, position=spaz.node.position) + light = bs.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + bs.timer(0.5, light.delete) + + # custom + spaz._punch_cooldown = self._snowball_rate + spaz.snowball_scale = self._snowball_scale + spaz.snowball_melt = self._snowball_melt + spaz.snowball_bounce = self._snowball_bounce + spaz.snowball_explode = self._snowball_explode + + return spaz + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + 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, bs.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: + self._dingsound.play() + 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 + self._dingsound.play() + + # 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): + bs.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 = bs.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/plugins/minigames/meteorshowerdeluxe.py b/plugins/minigames/meteorshowerdeluxe.py new file mode 100644 index 0000000..296c8dc --- /dev/null +++ b/plugins/minigames/meteorshowerdeluxe.py @@ -0,0 +1,67 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +""" +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. <[1](https://fsf.org/)> + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This license is designed to ensure cooperation with the community in the case of network server software. It is a free, copyleft license for software and other kinds of works. The license guarantees your freedom to share and change all versions of a program, to make sure it remains free software for all its users. + +The license identifier refers to the choice to use code under AGPL-3.0-or-later (i.e., AGPL-3.0 or some later version), as distinguished from use of code under AGPL-3.0-only. The license notice states which of these applies the code in the file. + + +""" +import random +import babase +import bauiv1 as bui +import bascenev1 as bs +from bascenev1lib.game.meteorshower import MeteorShowerGame +from bascenev1lib.actor.bomb import Bomb + + +class NewMeteorShowerGame(MeteorShowerGame): + @classmethod + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + return bs.app.classic.getmaps("melee") + + def _drop_bomb_cluster(self) -> None: + # Drop several bombs in series. + delay = 0.0 + bounds = list(self._map.get_def_bound_box("map_bounds")) + for _i in range(random.randrange(1, 3)): + # Drop them somewhere within our bounds with velocity pointing + # toward the opposite side. + pos = ( + random.uniform(bounds[0], bounds[3]), + bounds[4], + random.uniform(bounds[2], bounds[5]), + ) + dropdirx = -1 if pos[0] > 0 else 1 + dropdirz = -1 if pos[2] > 0 else 1 + forcex = ( + bounds[0] - bounds[3] + if bounds[0] - bounds[3] > 0 + else -(bounds[0] - bounds[3]) + ) + forcez = ( + bounds[2] - bounds[5] + if bounds[2] - bounds[5] > 0 + else -(bounds[2] - bounds[5]) + ) + vel = ( + (-5 + random.random() * forcex) * dropdirx, + random.uniform(-3.066, -4.12), + (-5 + random.random() * forcez) * dropdirz, + ) + bs.timer(delay, babase.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + +# ba_meta export plugin +class byEra0S(babase.Plugin): + MeteorShowerGame.get_supported_maps = NewMeteorShowerGame.get_supported_maps + MeteorShowerGame._drop_bomb_cluster = NewMeteorShowerGame._drop_bomb_cluster diff --git a/plugins/minigames/ofuuuAttack.py b/plugins/minigames/ofuuuAttack.py new file mode 100644 index 0000000..49b2afb --- /dev/null +++ b/plugins/minigames/ofuuuAttack.py @@ -0,0 +1,340 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor.bomb import BombFactory, Bomb +from bascenev1lib.gameutils import SharedObjects +from bascenev1lib.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List, Dict, Type, Type + +class _GotTouched(): + pass + +class UFO(bs.Actor): + + def __init__(self, pos: float = (0,0,0)): + super().__init__() + shared = SharedObjects.get() + self.r: Optional[int] = 0 + self.dis: Optional[List] = [] + self.target: float = (0.0, 0.0, 0.0) + self.regs: List[bs.NodeActor] = [] + self.node = bs.newnode('prop', + delegate=self, + attrs={'body':'landMine', + 'position': pos, + 'mesh':bs.getmesh('landMine'), + 'mesh_scale': 1.5, + 'body_scale': 0.01, + 'shadow_size': 0.000001, + 'gravity_scale': 0.0, + 'color_texture': bs.gettexture("achievementCrossHair"), + 'materials': [shared.object_material]}) + self.ufo_collide = None + + def create_target(self): + if not self.node.exists(): return + self.dis = [] + shared = SharedObjects.get() + try: + def pass_(): + self.regs.clear() + bs.timer(3875*0.001, self.move) + try: bs.timer(3277*0.001, lambda: Bomb(velocity=(0,0,0), position=(self.target[0], self.node.position[1]-0.43999, self.target[2]), bomb_type='impact').autoretain().arm()) + except: pass + key = bs.Material() + key.add_actions( + conditions=('they_have_material', shared.object_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', pass_()), + )) + except: pass + self.regs.append(bs.NodeActor(bs.newnode('region', + attrs={ + 'position': self.target, + 'scale': (0.04, 22, 0.04), + 'type': 'sphere', + 'materials':[key]}))) + + def move(self): + if not self.node.exists(): return + try: + self.create_target() + for j in bs.getnodes(): + n = j.getdelegate(object) + if j.getnodetype() == 'prop' and isinstance(n, TileFloor): + if n.node.exists(): self.dis.append(n.node) + self.r = random.randint(0,len(self.dis)-1) + self.target = (self.dis[self.r].position[0], self.node.position[1], self.dis[self.r].position[2]) + bs.animate_array(self.node, 'position', 3, { + 0:self.node.position, + 3.0:self.target}) + except: pass + def handlemessage(self, msg): + + if isinstance(msg, bs.DieMessage): + self.node.delete() + elif isinstance(msg ,bs.OutOfBoundsMessage): self.handlemessage(bs.DieMessage()) + else: super().handlemessage(msg) + + +class TileFloor(bs.Actor): + def __init__(self, + pos: float = (0, 0, 0)): + super().__init__() + get_mat = SharedObjects.get() + self.pos = pos + self.scale = 1.5 + self.mat, self.mat2, self.test = bs.Material(), bs.Material(), bs.Material() + self.mat.add_actions(conditions=('we_are_older_than', 1), actions=(('modify_part_collision', 'collide', False))) + self.mat2.add_actions(conditions=('we_are_older_than', 1), actions=(('modify_part_collision', 'collide', True))) + self.test.add_actions( + conditions=('they_have_material', BombFactory.get().bomb_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('message', 'our_node', 'at_connect', _GotTouched()))) + self.node = bs.newnode('prop', + delegate=self, + attrs={'body':'puck', + 'position': self.pos, + 'mesh':bs.getmesh('buttonSquareOpaque'), + 'mesh_scale': self.scale*1.16, + 'body_scale': self.scale, + 'shadow_size': 0.0002, + 'gravity_scale': 0.0, + 'color_texture': bs.gettexture("tnt"), + 'is_area_of_interest': True, + 'materials': [self.mat, self.test]}) + self.node_support = bs.newnode('region', + attrs={ + 'position': self.pos, + 'scale': (self.scale*0.8918, 0.1, self.scale*0.8918), + 'type': 'box', + 'materials':[get_mat.footing_material, self.mat2] + }) + def handlemessage(self, msg): + if isinstance(msg, bs.DieMessage): + self.node.delete() + self.node_support.delete() + elif isinstance(msg, _GotTouched): + def do(): self.handlemessage(bs.DieMessage()) + bs.timer(0.1, do) + else: super().handlemessage(msg) + +class defs(): + points = boxes = {} + boxes['area_of_interest_bounds'] = (-1.3440, 1.185751251, 3.7326226188) + ( + 0.0, 0.0, 0.0) + (29.8180273, 15.57249038, 22.93859993) + boxes['map_bounds'] = (0.0, 2.585751251, 0.4326226188) + (0.0, 0.0, 0.0) + (29.09506485, 15.81173179, 33.76723155) + +class DummyMapForGame(bs.Map): + defs, name = defs(), 'Tile Lands' + @classmethod + def get_play_types(cls) -> List[str]: + return [] + @classmethod + def get_preview_texture_name(cls) -> str: + return 'achievementCrossHair' + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = {'bg_1': bs.gettexture('rampageBGColor'),'bg_2': bs.gettexture('rampageBGColor2'),'bg_mesh_1': bs.getmesh('rampageBG'),'bg_mesh_2': bs.getmesh('rampageBG2'),} + return data + def __init__(self) -> None: + super().__init__() + self.bg1 = bs.newnode('terrain',attrs={'mesh': self.preloaddata['bg_mesh_1'],'lighting': False,'background': True,'color_texture': self.preloaddata['bg_2']}) + self.bg2 = bs.newnode('terrain',attrs={ 'mesh': self.preloaddata['bg_mesh_2'], 'lighting': False,'background': True, 'color_texture': self.preloaddata['bg_2']}) + a = bs.getactivity().globalsnode + a.tint, a.ambient_color, a.vignette_outer, a.vignette_inner = (1.2, 1.1, 0.97), (1.3, 1.2, 1.03), (0.62, 0.64, 0.69), (0.97, 0.95, 0.93) + +class DummyMapForGame2(bs.Map): + defs, name = defs(), 'Tile Lands Night' + @classmethod + def get_play_types(cls) -> List[str]: + return [] + @classmethod + def get_preview_texture_name(cls) -> str: + return 'achievementCrossHair' + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = {'bg_1': bs.gettexture('menuBG'),'bg_2': bs.gettexture('menuBG'),'bg_mesh_1': bs.getmesh('thePadBG'),'bg_mesh_2': bs.getmesh('thePadBG'),} + return data + def __init__(self) -> None: + super().__init__() + self.bg1 = bs.newnode('terrain',attrs={'mesh': self.preloaddata['bg_mesh_1'],'lighting': False,'background': True,'color_texture': self.preloaddata['bg_2']}) + self.bg2 = bs.newnode('terrain',attrs={ 'mesh': self.preloaddata['bg_mesh_2'], 'lighting': False,'background': True, 'color_texture': self.preloaddata['bg_2']}) + a = bs.getactivity().globalsnode + a.tint, a.ambient_color, a.vignette_outer, a.vignette_inner = (0.5, 0.7, 1.27), (2.5, 2.5, 2.5), (0.62, 0.64, 0.69), (0.97, 0.95, 0.93) + +bs._map.register_map(DummyMapForGame) +bs._map.register_map(DummyMapForGame2) + + + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export bascenev1.GameActivity +class UFOAttackGame(bs.TeamGameActivity[Player, Team]): + + name = 'UFO Attack' + description = 'Dodge the falling bombs.' + available_settings = [ + bs.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Enable Run', default=True), + bs.BoolSetting('Enable Jump', default=True), + bs.BoolSetting('Display Map Area Dimension', default=False), + bs.IntSetting('No. of Rows' + u' →',max_value=13, min_value=1, default=8, increment=1), + bs.IntSetting('No. of Columns' + u' ↓', max_value=12, min_value=1, default=6, increment=1) + ] + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, + version='B') + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + @classmethod + def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: + return ['Tile Lands', 'Tile Lands Night'] + @classmethod + def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: + return (issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + + self.col = int(settings['No. of Columns' + u' ↓']) + self.row = int(settings['No. of Rows' + u' →']) + self.bool1 = bool(settings['Enable Run']) + self.bool2 = bool(settings['Enable Jump']) + self._epic_mode = settings.get('Epic Mode', False) + self._last_player_death_time: Optional[float] = None + self._timer: Optional[OnScreenTimer] = None + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + if bool(settings["Display Map Area Dimension"]): + self.game_name = "UFO Attack " + "(" + str(self.col) + "x" + str(self.row) + ")" + else: self.game_name = "UFO Attack" + if self._epic_mode: + self.slow_motion = True + + def get_instance_display_string(self) -> babase.Lstr: + return self.game_name + + def on_begin(self) -> None: + super().on_begin() + self._timer = OnScreenTimer() + self._timer.start() + #bs.timer(5.0, self._check_end_game) + for r in range(self.col): + for j in range(self.row): + tile = TileFloor(pos=(-6.204283+(j*1.399), 3.425666, + -1.3538+(r*1.399))).autoretain() + self.ufo = UFO(pos=(-5.00410667, 6.616383286, -2.503472)).autoretain() + bs.timer(7000*0.001, lambda: self.ufo.move()) + for t in self.players: + self.spawn_player(t) + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + assert self._timer is not None + player.death_time = self._timer.getstarttime() + return + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + self._check_end_game() + + def spawn_player(self, player: Player) -> bs.Actor: + dis = [] + for a in bs.getnodes(): + g = a.getdelegate(object) + if a.getnodetype() == 'prop' and isinstance(g, TileFloor): + dis.append(g.node) + r = random.randint(0, len(dis)-1) + spaz = self.spawn_player_spaz(player, position=(dis[r].position[0], dis[r].position[1]+1.005958, dis[r].position[2])) + spaz.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_run=self.bool1, + enable_jump=self.bool2, + enable_pickup=False) + spaz.play_big_death_sound = True + return spaz + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + super().handlemessage(msg) + + curtime = bs.time() + msg.getplayer(Player).death_time = curtime + bs.timer(1.0, self._check_end_game) + + else: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + 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 <= 1: + self.end_game() + + def end_game(self) -> None: + self.ufo.handlemessage(bs.DieMessage()) + cur_time = bs.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + for team in self.teams: + for player in team.players: + survived = False + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 2 + self.stats.player_scored(player, score, screenmessage=False) + self._timer.stop(endtime=self._last_player_death_time) + results = bs.GameResults() + for team in self.teams: + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, + player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(longest_life)) + + self.end(results=results) \ No newline at end of file diff --git a/plugins/minigames/safe_zone.py b/plugins/minigames/safe_zone.py new file mode 100644 index 0000000..4c2620f --- /dev/null +++ b/plugins/minigames/safe_zone.py @@ -0,0 +1,720 @@ +# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport) +# Released under the MIT License. See LICENSE for details. +# +"""Elimination mini-game.""" + +# Maded by Froshlee14 +# Update by SEBASTIAN2059 + +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase +import random +from bascenev1lib.actor.spazfactory import SpazFactory +from bascenev1lib.actor.scoreboard import Scoreboard +from bascenev1lib.actor import spazbot as stdbot +from bascenev1lib.gameutils import SharedObjects as so + +if TYPE_CHECKING: + from typing import (Any, Tuple, Dict, Type, List, Sequence, Optional, + Union) + + +class Icon(bs.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: Tuple[float, float], + scale: float, + show_lives: bool = True, + show_death: bool = True, + name_scale: float = 1.0, + name_maxwidth: float = 115.0, + flatness: float = 1.0, + shadow: float = 1.0): + super().__init__() + + self._player = player + self._show_lives = show_lives + self._show_death = show_death + self._name_scale = name_scale + self._outline_tex = bs.gettexture('characterIconMask') + + icon = player.get_icon() + self.node = bs.newnode('image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + self._name_text = bs.newnode( + 'text', + owner=self.node, + attrs={ + 'text': babase.Lstr(value=player.getname()), + 'color': babase.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': flatness, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + if self._show_lives: + self._lives_text = bs.newnode('text', + owner=self.node, + attrs={ + 'text': 'x0', + 'color': (1, 1, 0.5), + 'h_align': 'left', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_position_and_scale(position, scale) + + def set_position_and_scale(self, position: Tuple[float, float], + scale: float) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + if self._show_lives: + self._lives_text.position = (position[0] + scale * 10.0, + position[1] - scale * 43.0) + self._lives_text.scale = 1.0 * scale + + def update_for_lives(self) -> None: + """Update for the target player's current lives.""" + if self._player: + lives = self._player.lives + else: + lives = 0 + if self._show_lives: + if lives > 0: + self._lives_text.text = 'x' + str(lives - 1) + else: + self._lives_text.text = '' + if lives == 0: + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + def handle_player_spawned(self) -> None: + """Our player spawned; hooray!""" + if not self.node: + return + self.node.opacity = 1.0 + self.update_for_lives() + + def handle_player_died(self) -> None: + """Well poo; our player died.""" + if not self.node: + return + if self._show_death: + bs.animate( + self.node, 'opacity', { + 0.00: 1.0, + 0.05: 0.0, + 0.10: 1.0, + 0.15: 0.0, + 0.20: 1.0, + 0.25: 0.0, + 0.30: 1.0, + 0.35: 0.0, + 0.40: 1.0, + 0.45: 0.0, + 0.50: 1.0, + 0.55: 0.2 + }) + lives = self._player.lives + if lives == 0: + bs.timer(0.6, self.update_for_lives) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.DieMessage): + self.node.delete() + return None + return super().handlemessage(msg) + + +class Player(bs.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.lives = 0 + self.icons: List[Icon] = [] + + +class Team(bs.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: Optional[int] = None + self.spawn_order: List[Player] = [] + +lang = bs.app.lang.language +if lang == 'Spanish': + description = 'Mantente en la zona segura.' + join_description = 'Corre hacia la zona segura.' + kill_timer = 'Kill timer: ' +else: + description = 'Stay in the safe zone.' + join_description = 'Run into the safe zone' + kill_timer = 'Kill timer: ' + +# ba_meta export bascenev1.GameActivity +class SafeZoneGame(bs.TeamGameActivity[Player, Team]): + """Game type where last player(s) left alive win.""" + + name = 'Safe Zone' + description = description + scoreconfig = bs.ScoreConfig(label='Survived', + scoretype=bs.ScoreType.SECONDS, + none_is_winner=True) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]: + settings = [ + bs.IntSetting( + 'Lives Per Player', + default=2, + min_value=1, + max_value=10, + 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=[ + ('Short', 0.25), + ('Normal', 0.5), + ], + default=0.5, + ), + bs.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) + settings.append( + bs.BoolSetting('Balance Total Lives', 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 ['Football Stadium','Hockey Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._start_time: Optional[float] = None + self._vs_text: Optional[bs.Actor] = None + self._round_end_timer: Optional[bs.Timer] = None + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = int(settings['Lives Per Player']) + self._time_limit = float(settings['Time Limit']) + self._balance_total_lives = bool( + settings.get('Balance Total Lives', False)) + self._solo_mode = bool(settings.get('Solo Mode', False)) + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = (bs.MusicType.EPIC + if self._epic_mode else bs.MusicType.SURVIVAL) + + self._tick_sound = bs.getsound('tick') + + def get_instance_description(self) -> Union[str, Sequence]: + return join_description + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'last team standing wins' if isinstance( + self.session, bs.DualTeamSession) else 'last one standing wins' + + def on_player_join(self, player: Player) -> None: + + # No longer allowing mid-game joiners here; too easy to exploit. + if self.has_begun(): + + # Make sure their team has survival seconds set if they're all dead + # (otherwise blocked new ffa players are considered 'still alive' + # in score tallying). + if (self._get_total_team_lives(player.team) == 0 + and player.team.survival_seconds is None): + player.team.survival_seconds = 0 + bs.broadcastmessage( + babase.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + return + + player.lives = self._lives_per_player + + if self._solo_mode: + player.team.spawn_order.append(player) + self._update_solo_mode() + else: + # Create our icon and spawn. + player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: + self.spawn_player(player) + + # Don't waste time doing this until begin. + if self.has_begun(): + self._update_icons() + + def on_begin(self) -> None: + super().on_begin() + self._start_time = bs.time() + self.setup_standard_time_limit(self._time_limit) + #self.setup_standard_powerup_drops() + + bs.timer(5,self.spawn_zone) + self._bots = stdbot.SpazBotSet() + bs.timer(3,babase.Call(self.add_bot,'left')) + bs.timer(3,babase.Call(self.add_bot,'right')) + if len(self.initialplayerinfos) > 4: + bs.timer(5,babase.Call(self.add_bot,'right')) + bs.timer(5,babase.Call(self.add_bot,'left')) + + if self._solo_mode: + self._vs_text = bs.NodeActor( + bs.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': babase.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, bs.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + bs.timer(1.0, self._update, repeat=True) + + def spawn_zone(self): + self.zone_pos = (random.randrange(-10,10),0.05,random.randrange(-5,5)) + self.zone = bs.newnode('locator',attrs={'shape':'circle','position':self.zone_pos,'color':(1, 1, 0),'opacity':0.8,'draw_beauty':True,'additive':False,'drawShadow':False}) + self.zone_limit = bs.newnode('locator',attrs={'shape':'circleOutline','position':self.zone_pos,'color':(1, 0.2, 0.2),'opacity':0.8,'draw_beauty':True,'additive':False,'drawShadow':False}) + bs.animate_array(self.zone, 'size', 1,{0:[0], 0.3:[self.get_players_count()*0.85], 0.35:[self.get_players_count()*0.8]}) + bs.animate_array(self.zone_limit, 'size', 1,{0:[0], 0.3:[self.get_players_count()*1.2], 0.35:[self.get_players_count()*0.95]}) + self.last_players_count = self.get_players_count() + bs.getsound('laserReverse').play() + self.start_timer() + self.move_zone() + + def delete_zone(self): + self.zone.delete() + self.zone = None + self.zone_limit.delete() + self.zone_limit = None + bs.getsound('shieldDown').play() + bs.timer(1,self.spawn_zone) + + def move_zone(self): + if self.zone_pos[0] > 0: x = random.randrange(0,10) + else: x = random.randrange(-10,0) + + if self.zone_pos[2] > 0: y = random.randrange(0,5) + else: y = random.randrange(-5,0) + + new_pos = (x,0.05,y) + bs.animate_array(self.zone, 'position', 3,{0:self.zone.position, 8:new_pos}) + bs.animate_array(self.zone_limit, 'position', 3,{0:self.zone_limit.position,8:new_pos}) + + def start_timer(self): + count = self.get_players_count() + self._time_remaining = 10 if count > 9 else count-1 if count > 6 else count if count > 2 else count*2 + self._timer_x = bs.Timer(1.0,bs.WeakCall(self.tick),repeat=True) + # gnode = bs.getactivity().globalsnode + # tint = gnode.tint + # bs.animate_array(gnode,'tint',3,{0:tint,self._time_remaining*1.5:(1.0,0.5,0.5),self._time_remaining*1.55:tint}) + + def stop_timer(self): + self._time = None + self._timer_x = None + + def tick(self): + self.check_players() + self._time = bs.NodeActor(bs.newnode('text', + attrs={'v_attach':'top','h_attach':'center', + 'text':kill_timer+str(self._time_remaining)+'s', + 'opacity':0.8,'maxwidth':100,'h_align':'center', + 'v_align':'center','shadow':1.0,'flatness':1.0, + 'color':(1,1,1),'scale':1.5,'position':(0,-50)} + ) + ) + self._time_remaining -= 1 + self._tick_sound.play() + + def check_players(self): + if self._time_remaining <= 0: + self.stop_timer() + bs.animate_array(self.zone, 'size', 1,{0:[self.last_players_count*0.8], 1.4:[self.last_players_count*0.8],1.5:[0]}) + bs.animate_array(self.zone_limit, 'size', 1,{0:[self.last_players_count*0.95], 1.45:[self.last_players_count*0.95],1.5:[0]}) + bs.timer(1.5,self.delete_zone) + for player in self.players: + if not player.actor is None: + if player.actor.is_alive(): + p1 = player.actor.node.position + p2 = self.zone.position + diff = (babase.Vec3(p1[0]-p2[0],0.0,p1[2]-p2[2])) + dist = (diff.length()) + if dist > (self.get_players_count()*0.7): + player.actor.handlemessage(bs.DieMessage()) + + def get_players_count(self): + count = 0 + for player in self.players: + if not player.actor is None: + if player.actor.is_alive(): + count += 1 + return count + + def _update_solo_mode(self) -> None: + # For both teams, find the first player on the spawn order list with + # lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + break + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, bs.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon(player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False)) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def _get_spawn_point(self, player: Player) -> Optional[babase.Vec3]: + del player # Unused. + + # In solo-mode, if there's an existing live player on the map, spawn at + # whichever spot is farthest from them (keeps the action spread out). + if self._solo_mode: + living_player = None + living_player_pos = None + for team in self.teams: + for tplayer in team.players: + if tplayer.is_alive(): + assert tplayer.node + ppos = tplayer.node.position + living_player = tplayer + living_player_pos = ppos + break + if living_player: + assert living_player_pos is not None + player_pos = babase.Vec3(living_player_pos) + points: List[Tuple[float, babase.Vec3]] = [] + for team in self.teams: + start_pos = babase.Vec3(self.map.get_start_position(team.id)) + points.append( + ((start_pos - player_pos).length(), start_pos)) + # Hmm.. we need to sorting vectors too? + points.sort(key=lambda x: x[0]) + return points[-1][1] + return None + + def spawn_player(self, player: Player) -> bs.Actor: + actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) + if not self._solo_mode: + bs.timer(0.3, babase.Call(self._print_lives, player)) + + # spaz but *without* the ability to attack or pick stuff up. + actor.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_pickup=False) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + return actor + + def _print_lives(self, player: Player) -> None: + from bascenev1lib.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + popuptext.PopupText('x' + str(player.lives - 1), + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=player.node.position).autoretain() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + player.icons = [] + + # Remove us from spawn-order. + if self._solo_mode: + if player in player.team.spawn_order: + player.team.spawn_order.remove(player) + + # Update icons in a moment since our team will be gone from the + # list then. + bs.timer(0, self._update_icons) + + # If the player to leave was the last in spawn order and had + # their final turn currently in-progress, mark the survival time + # for their team. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - self._start_time) + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, bs.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + babase.print_error( + "Got lives < 0 in Elim; this shouldn't happen. solo:" + + str(self._solo_mode)) + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if self._solo_mode or player.lives == 0: + SpazFactory.get().single_player_death_sound.play() + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + # If the whole team is now dead, mark their survival time. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(bs.time() - + self._start_time) + else: + # Otherwise, in regular mode, respawn. + if not self._solo_mode: + self.respawn_player(player) + + # In solo, put ourself at the back of the spawn order. + if self._solo_mode: + player.team.spawn_order.remove(player) + player.team.spawn_order.append(player) + elif isinstance(msg,stdbot.SpazBotDiedMessage): + self._on_spaz_bot_died(msg) + + def _on_spaz_bot_died(self,die_msg): + bs.timer(1,babase.Call(self.add_bot,die_msg.spazbot.node.position)) + + def _on_bot_spawn(self,spaz): + spaz.update_callback = self.move_bot + spaz_type = type(spaz) + spaz._charge_speed = self._get_bot_speed(spaz_type) + + def add_bot(self,pos=None): + if pos == 'left': position = (-11,0,random.randrange(-5,5)) + elif pos == 'right': position = (11,0,random.randrange(-5,5)) + else: position = pos + self._bots.spawn_bot(self.get_random_bot(),pos=position,spawn_time=1,on_spawn_call=babase.Call(self._on_bot_spawn)) + + def move_bot(self,bot): + p = bot.node.position + speed = -bot._charge_speed if(p[0]>=-11 and p[0]<0) else bot._charge_speed + + if (p[0]>=-11) and (p[0]<=11): + bot.node.move_left_right = speed + bot.node.move_up_down = 0.0 + bot.node.run = 0.0 + return True + return False + + def get_random_bot(self): + bots = [stdbot.BomberBotStatic, stdbot.TriggerBotStatic] + return (random.choice(bots)) + + def _get_bot_speed(self, bot_type): + if bot_type == stdbot.BomberBotStatic: + return 0.48 + elif bot_type == stdbot.TriggerBotStatic: + return 0.73 + else: + raise Exception('Invalid bot type to _getBotSpeed(): '+str(bot_type)) + + def _update(self) -> None: + if self._solo_mode: + # For both teams, find the first player on the spawn order + # list with lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + self._update_icons() + break + + # If we're down to 1 or fewer living teams, start a timer to end + # the game (allows the dust to settle and draws to occur if deaths + # are close enough). + if len(self._get_living_teams()) < 2: + self._round_end_timer = bs.Timer(0.5, self.end_game) + + def _get_living_teams(self) -> List[Team]: + return [ + team for team in self.teams + if len(team.players) > 0 and any(player.lives > 0 + for player in team.players) + ] + + def end_game(self) -> None: + if self.has_ended(): + return + results = bs.GameResults() + self._vs_text = None # Kill our 'vs' if its there. + for team in self.teams: + results.set_team_score(team, team.survival_seconds) + self.end(results=results) diff --git a/plugins/utilities.json b/plugins/utilities.json index 5404ffd..0b16c15 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -986,6 +986,62 @@ "md5sum": "5fa8706f36d618f8302551dd2a0403a0" } } - } + }, + "disable_friendly_fire": { + "description": "Disables friendly fire", + "external_url": "", + "authors": [ + { + "name": "EmperoR", + "email": "", + "discord": "EmperoR#4098" + } + ], + "versions": { + "1.0.0": null + } + }, + "infinityShield": { + "description": "Gives you unbreakable shield", + "external_url": "https://youtu.be/hp7vbB-hUPg?si=i7Th0NP5xDPLN2P_", + "authors": [ + { + "name": "JoseAng3l", + "email": "", + "discord": "joseang3l" + } + ], + "versions": { + "1.0.0": null + } + }, + "OnlyNight": { + "description": "Night Mode", + "external_url": "", + "authors": [ + { + "name": "", + "email": "", + "discord": "" + } + ], + "versions": { + "1.0.0": null + } + }, + "Tag": { + "description": "Get a tag", + "external_url": "", + "authors": [ + { + "name": "pranav", + "email": "", + "discord": "" + } + ], + "versions": { + "2.0.1": null + } + } } } \ No newline at end of file diff --git a/plugins/utilities/InfinityShield.py b/plugins/utilities/InfinityShield.py new file mode 100644 index 0000000..a224ba3 --- /dev/null +++ b/plugins/utilities/InfinityShield.py @@ -0,0 +1,81 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import random +from bascenev1lib.actor.spaz import Spaz +from bascenev1lib.actor.spazfactory import SpazFactory + +if TYPE_CHECKING: + pass + + +Spaz._old_init = Spaz.__init__ +def __init__(self, + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + character: str = 'Spaz', + source_player: bs.Player = None, + start_invincible: bool = True, + can_accept_powerups: bool = True, + powerups_expire: bool = False, + demo_mode: bool = False): + self._old_init(color,highlight,character,source_player,start_invincible, + can_accept_powerups,powerups_expire,demo_mode) + if self.source_player: + self.equip_shields() + def animate_shield(): + if not self.shield: + return + bs.animate_array(self.shield, 'color', 3, { + 0.0: self.shield.color, + 0.2: (random.random(), random.random(), random.random()) + }) + bs.timer(0.2, animate_shield, repeat=True) + self.impact_scale = 0 + +def equip_shields(self, decay: bool = False) -> None: + """ + Give this spaz a nice energy shield. + """ + + if not self.node: + babase.print_error('Can\'t equip shields; no node.') + return + + factory = SpazFactory.get() + if self.shield is None: + self.shield = bs.newnode('shield', + owner=self.node, + attrs={ + 'color': (0.3, 0.2, 2.0), + 'radius': 1.3 + }) + self.node.connectattr('position_center', self.shield, 'position') + self.shield_hitpoints = self.shield_hitpoints_max = 650 + self.shield_decay_rate = factory.shield_decay_rate if decay else 0 + self.shield.hurt = 0 + factory.shield_up_sound.play(1.0, position=self.node.position) + + if self.impact_scale == 0: + return + + if self.shield_decay_rate > 0: + self.shield_decay_timer = bs.Timer(0.5, + bs.WeakCall(self.shield_decay), + repeat=True) + # So user can see the decay. + self.shield.always_show_health_bar = True + + +# ba_meta export plugin +class InfinityShieldPlugin(babase.Plugin): + Spaz.__init__ = __init__ + Spaz.equip_shields = equip_shields diff --git a/plugins/utilities/OnlyNight.py b/plugins/utilities/OnlyNight.py new file mode 100644 index 0000000..b337647 --- /dev/null +++ b/plugins/utilities/OnlyNight.py @@ -0,0 +1,50 @@ +# Ported by brostos to api 8 +# Tool used to make porting easier.(https://github.com/bombsquad-community/baport) +"""Only Night.""" + +# ba_meta require api 8 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import babase +import bascenev1 as bs +from bascenev1._gameactivity import GameActivity + +if TYPE_CHECKING: + pass + + +# ba_meta export plugin +class OnlyNight(babase.Plugin): + GameActivity.old_on_transition_in = GameActivity.on_transition_in + + def new_on_transition_in(self) -> None: + self.old_on_transition_in() + gnode = bs.getactivity().globalsnode + if self.map.getname() in [ + "Monkey Face", + "Rampage", + "Roundabout", + "Step Right Up", + "Tip Top", + "Zigzag", + "The Pad", + ]: + gnode.tint = (0.4, 0.4, 0.4) + elif self.map.getname() in [ + "Big G", + "Bridgit", + "Courtyard", + "Crag Castle", + "Doom Shroom", + "Football Stadium", + "Happy Thoughts", + "Hockey Stadium", + ]: + gnode.tint = (0.5, 0.5, 0.5) + else: + gnode.tint = (0.3, 0.3, 0.3) + + GameActivity.on_transition_in = new_on_transition_in diff --git a/plugins/utilities/Tag.py b/plugins/utilities/Tag.py new file mode 100644 index 0000000..4ec9a9d --- /dev/null +++ b/plugins/utilities/Tag.py @@ -0,0 +1,565 @@ +# Ported by brostos to api 8 +# Tool used to make porting easier.(https://github.com/bombsquad-community/baport) +""" +I apreciate any kind of modification. So feel free to use or edit code or change credit string.... no problem. + +really awsome servers: + Bombsquad Consultancy Service - https://discord.gg/2RKd9QQdQY + bombspot - https://discord.gg/ucyaesh + cyclones - https://discord.gg/pJXxkbQ7kH + +how to use: + Account -> PlayerProfile -> Edit(new profile -> edit) + Open profile you like (every profile has dirrent tags, settings (Configs)) + enable tag for profile you like, edit tag you want. enable cool flashy animation +""" + +from __future__ import annotations +from bauiv1lib.profile.edit import EditProfileWindow +from bauiv1lib.colorpicker import ColorPicker +from bauiv1lib.popup import PopupMenu +from bascenev1lib.actor.playerspaz import PlayerSpaz +from baenv import TARGET_BALLISTICA_BUILD as build_number +import babase +import bauiv1 as bui +import bascenev1 as bs +import _babase + +from typing import ( + Tuple, + Optional, + Sequence, + Union, + Callable, + Any, + List, + cast +) + +__version__ = 2.0 +__author__ = "pranav1711#2006" + + +# Default Confings/Settings +Configs = { + "enabletag": False, + "tag": "", + "scale": "medium", + "opacity": 1.0, + "shadow": 0.0, + "animtag": False, + "frequency": 0.5 +} + +# Useful global fucntions +def setconfigs() -> None: + """ + Set required defualt configs for mod + """ + cnfg = babase.app.config + profiles = cnfg['Player Profiles'] + if not "TagConf" in cnfg: cnfg["TagConf"] = {} + for p in profiles: + if not p in cnfg["TagConf"]: + cnfg["TagConf"][str(p)] = Configs + babase.app.config.apply_and_commit() + +def getanimcolor(name: str) -> dict: + """ + Returns dictnary of colors with prefective time -> {seconds: (r, g, b)} + """ + freq = babase.app.config['TagConf'][str(name)]['frequency'] + s1 = 0.0 + s2 = s1 + freq + s3 = s2 + freq + + animcolor = { + s1: (1,0,0), + s2: (0,1,0), + s3: (0,0,1) + } + return animcolor + +def gethostname() -> str: + """ + Return player name, by using -1 only host can use tags. + """ + session = bs.get_foreground_host_session() + with session.context: + for player in session.sessionplayers: + if player.inputdevice.client_id == -1: + name = player.getname(full=True, icon=False) + break + if name == bui.app.plus.get_v1_account_name: + return '__account__' + return name + +# Dummy functions for extend functionality for class object +PlayerSpaz.init = PlayerSpaz.__init__ +EditProfileWindow.init = EditProfileWindow.__init__ + +# PlayerSpaz object at -> bascenev1lib.actor.playerspaz +def NewPlayerSzapInit(self, + player: bs.Player, + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + character: str = 'Spaz', + powerups_expire: bool = True) -> None: + self.init(player, color, highlight, character, powerups_expire) + self.curname = gethostname() + + try: + cnfg = babase.app.config["TagConf"] + if cnfg[str(self.curname)]["enabletag"]: + # Tag node + self.mnode = bs.newnode('math', owner=self.node, attrs={'input1': (0, 1.5, 0),'operation': 'add'}) + self.node.connectattr('torso_position', self.mnode, 'input2') + + tagtext = cnfg[str(self.curname)]["tag"] + opacity = cnfg[str(self.curname)]["opacity"] + shadow = cnfg[str(self.curname)]["shadow"] + sl = cnfg[str(self.curname)]["scale"] + scale = 0.01 if sl == 'mediam' else 0.009 if not sl == 'large' else 0.02 + + self.Tag = bs.newnode( + type='text', + owner=self.node, + attrs={ + 'text': str(tagtext), + 'in_world': True, + 'shadow': shadow, + 'color': (0,0,0), + 'scale': scale, + 'opacity': opacity, + 'flatness': 1.0, + 'h_align': 'center'}) + self.mnode.connectattr('output', self.Tag, 'position') + + if cnfg[str(self.curname)]["animtag"]: + kys = getanimcolor(self.curname) + bs.animate_array(node=self.Tag, attr='color', size=3, keys=kys, loop=True) + except Exception: pass + + +def NewEditProfileWindowInit(self, + existing_profile: Optional[str], + in_main_menu: bool, + transition: str = 'in_right') -> None: + """ + New boilerplate for editprofilewindow, addeds button to call TagSettings window + """ + self.existing_profile = existing_profile + self.in_main_menu = in_main_menu + self.init(existing_profile, in_main_menu, transition) + + v = self._height - 115.0 + x_inset = self._x_inset + b_width = 50 + b_height = 30 + + self.tagwinbtn = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(505 + x_inset, v - 38 - 15), + size=(b_width, b_height), + color=(0.6, 0.5, 0.6), + label='Tag', + button_type='square', + text_scale=1.2, + on_activate_call=babase.Call(_on_tagwinbtn_press, self)) + +def _on_tagwinbtn_press(self): + """ + Calls tag config window passes all paramisters + """ + bui.containerwidget(edit=self._root_widget, transition='out_scale') + bui.app.ui_v1.set_main_menu_window( + TagWindow(self.existing_profile, + self.in_main_menu, + self._name, + transition='in_right').get_root_widget(), from_window=self._root_widget) + + +# ba_meta require api 8 +# ba_meta export plugin +class Tag(babase.Plugin): + def __init__(self) -> None: + """ + Tag above actor player head, replacing PlayerSpaz class for getting actor, + using EditProfileWindow for UI. + """ + if _babase.env().get("build_number",0) >= 20327: + setconfigs() + self.Replace() + + def Replace(self) -> None: + """ + Replacing bolierplates no harm to relative funtionality only extending + """ + PlayerSpaz.__init__ = NewPlayerSzapInit + EditProfileWindow.__init__ = NewEditProfileWindowInit + + +class TagWindow(bui.Window): + + def __init__(self, + existing_profile: Optional[str], + in_main_menu: bool, + profilename: str, + transition: Optional[str] = 'in_right'): + self.existing_profile = existing_profile + self.in_main_menu = in_main_menu + self.profilename = profilename + + uiscale = bui.app.ui_v1.uiscale + self._width = 870.0 if uiscale is babase.UIScale.SMALL else 670.0 + self._height = (390.0 if uiscale is babase.UIScale.SMALL else + 450.0 if uiscale is babase.UIScale.MEDIUM else 520.0) + extra_x = 100 if uiscale is babase.UIScale.SMALL else 0 + self.extra_x = extra_x + top_extra = 20 if uiscale is babase.UIScale.SMALL else 0 + + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=(2.06 if uiscale is babase.UIScale.SMALL else + 1.4 if uiscale is babase.UIScale.MEDIUM else 1.0))) + + self._back_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + selectable=False, # FIXME: when press a in text field it selets to button + position=(52 + self.extra_x, self._height - 60), + size=(60, 60), + scale=0.8, + label=babase.charstr(babase.SpecialChar.BACK), + button_type='backSmall', + on_activate_call=self._back) + bui.containerwidget(edit=self._root_widget, cancel_button=self._back_button) + + self._save_button = bui.buttonwidget( + parent=self._root_widget, + position=(self._width - (177 + extra_x), + self._height - 60), + size=(155, 60), + color=(0, 0.7, 0.5), + autoselect=True, + selectable=False, # FIXME: when press a in text field it selets to button + scale=0.8, + label=babase.Lstr(resource='saveText'), + on_activate_call=self.on_save) + bui.widget(edit=self._save_button, left_widget=self._back_button) + bui.widget(edit=self._back_button, right_widget=self._save_button) + bui.containerwidget(edit=self._root_widget, start_button=self._save_button) + + self._title_text = bui.textwidget( + parent=self._root_widget, + position=(0, self._height - 52 - top_extra), + size=(self._width, 25), + text='Tag', + color=bui.app.ui_v1.title_color, + scale=1.5, + h_align='center', + v_align='top') + + self._scroll_width = self._width - (100 + 2 * extra_x) + self._scroll_height = self._height - 115.0 + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 724.0 + self._spacing = 32 + self._extra_button_spacing = self._spacing * 2.5 + + self._scrollwidget = bui.scrollwidget( + parent=self._root_widget, + position=(50 + extra_x, 50), + simple_culling_v=20.0, + highlight=False, + size=(self._scroll_width, + self._scroll_height), + selection_loops_to_parent=True) + bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) + + self._subcontainer = bui.containerwidget( + parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False, + selection_loops_to_parent=True) + + v = self._sub_height - 35 + v -= self._spacing * 1.2 + + self._prof = babase.app.config["TagConf"][self.profilename] + self.enabletagcb = bui.checkboxwidget( + parent=self._subcontainer, + autoselect=False, + position=(10.0, v + 30), + size=(10, 10), + text='Enable Tag', + textcolor=(0.8, 0.8, 0.8), + value=self._prof['enabletag'], + on_value_change_call=babase.Call(self.change_val, [f'{self.profilename}', 'enabletag']), + scale=1.1 if uiscale is babase.UIScale.SMALL else 1.5, + maxwidth=430) + + self.tag_text = bui.textwidget( + parent=self._subcontainer, + text='Tag', + position=(25.0, v - 30), + flatness=1.0, + scale=1.55, + maxwidth=430, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8)) + + self.tagtextfield = bui.textwidget( + parent=self._subcontainer, + position=(100.0, v - 45), + size=(350, 50), + text=self._prof["tag"], + h_align='center', + v_align='center', + max_chars=16, + autoselect=True, + editable=True, + padding=4, + color=(0.9, 0.9, 0.9, 1.0)) + + self.tag_color_text = bui.textwidget( + parent=self._subcontainer, + text='Color', + position=(40.0, v - 80), + flatness=1.0, + scale=1.25, + maxwidth=430, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8)) + + self.tag_scale_text = bui.textwidget( + parent=self._subcontainer, + text='Scale', + position=(40.0, v - 130), + flatness=1.0, + scale=1.25, + maxwidth=430, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8)) + + self.tag_scale_button = PopupMenu( + parent=self._subcontainer, + position=(330.0, v - 145), + width=150, + autoselect=True, + on_value_change_call=bs.WeakCall(self._on_menu_choice), + choices=['large', 'medium', 'small'], + button_size=(150, 50), + #choices_display=('large', 'medium', 'small'), + current_choice=self._prof["scale"]) + + CustomConfigNumberEdit( + parent=self._subcontainer, + position=(40.0, v - 180), + xoffset=65, + displayname='Opacity', + configkey=['TagConf', f'{self.profilename}', 'opacity'], + changesound=False, + minval=0.5, + maxval=2.0, + increment=0.1, + textscale=1.25) + + CustomConfigNumberEdit( + parent=self._subcontainer, + position=(40.0, v - 230), + xoffset=65, + displayname='Shadow', + configkey=['TagConf', f'{self.profilename}', 'shadow'], + changesound=False, + minval=0.0, + maxval=2.0, + increment=0.1, + textscale=1.25) + + self.enabletaganim = bui.checkboxwidget( + parent=self._subcontainer, + autoselect=True, + position=(10.0, v - 280), + size=(10, 10), + text='Animate tag', + textcolor=(0.8, 0.8, 0.8), + value=self._prof['enabletag'], + on_value_change_call=babase.Call(self.change_val, [f'{self.profilename}', 'animtag']), + scale=1.1 if uiscale is babase.UIScale.SMALL else 1.5, + maxwidth=430) + + CustomConfigNumberEdit( + parent=self._subcontainer, + position=(40.0, v - 330), + xoffset=65, + displayname='Frequency', + configkey=['TagConf', f'{self.profilename}', 'frequency'], + changesound=False, + minval=0.1, + maxval=5.0, + increment=0.1, + textscale=1.25) + + def _back(self) -> None: + """ + transit window into back window + """ + bui.containerwidget(edit=self._root_widget, + transition='out_scale') + bui.app.ui_v1.set_main_menu_window(EditProfileWindow( + self.existing_profile, + self.in_main_menu, + transition='in_left').get_root_widget(), from_window=self._root_widget) + + def change_val(self, config: List[str], val: bool) -> None: + """ + chamges the value of check boxes + """ + cnfg = babase.app.config["TagConf"] + try: + cnfg[config[0]][config[1]] = val + bui.getsound('gunCocking').play() + except Exception: + bui.screenmessage("error", color=(1,0,0)) + bui.getsound('error').play() + babase.app.config.apply_and_commit() + + def _on_menu_choice(self, choice: str): + """ + Changes the given choice in configs + """ + cnfg = babase.app.config["TagConf"][self.profilename] + cnfg["scale"] = choice + babase.app.config.apply_and_commit() + + def on_save(self): + """ + Gets the text in text field of tag and then save it + """ + text: str = cast(str, bui.textwidget(query=self.tagtextfield)) + profile = babase.app.config["TagConf"][self.profilename] + if not text == "" or not text.strip(): + profile['tag'] = text + babase.app.config.apply_and_commit() + bui.getsound('gunCocking').play() + else: + bui.screenmessage(f"please define tag", color=(1,0,0)) + bui.getsound('error').play() + + bui.containerwidget(edit=self._root_widget, + transition='out_scale') + bui.app.ui_v1.set_main_menu_window(EditProfileWindow( + self.existing_profile, + self.in_main_menu, + transition='in_left').get_root_widget(), from_window=self._root_widget) + + +class CustomConfigNumberEdit: + """A set of controls for editing a numeric config value. + + It will automatically save and apply the config when its + value changes. + + Attributes: + + nametext + The text widget displaying the name. + + valuetext + The text widget displaying the current value. + + minusbutton + The button widget used to reduce the value. + + plusbutton + The button widget used to increase the value. + """ + + def __init__(self, + parent: bui.Widget, + configkey: List[str], + position: Tuple[float, float], + minval: float = 0.0, + maxval: float = 100.0, + increment: float = 1.0, + callback: Callable[[float], Any] = None, + xoffset: float = 0.0, + displayname: Union[str, babase.Lstr] = None, + changesound: bool = True, + textscale: float = 1.0): + self._minval = minval + self._maxval = maxval + self._increment = increment + self._callback = callback + self._configkey = configkey + self._value = babase.app.config[configkey[0]][configkey[1]][configkey[2]] + + self.nametext = bui.textwidget( + parent=parent, + position=position, + size=(100, 30), + text=displayname, + maxwidth=160 + xoffset, + color=(0.8, 0.8, 0.8, 1.0), + h_align='left', + v_align='center', + scale=textscale) + + self.valuetext = bui.textwidget( + parent=parent, + position=(246 + xoffset, position[1]), + size=(60, 28), + editable=False, + color=(0.3, 1.0, 0.3, 1.0), + h_align='right', + v_align='center', + text=str(self._value), + padding=2) + + self.minusbutton = bui.buttonwidget( + parent=parent, + position=(330 + xoffset, position[1]), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=babase.Call(self._down), + repeat=True, + enable_sound=changesound) + + self.plusbutton = bui.buttonwidget(parent=parent, + position=(380 + xoffset, position[1]), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=babase.Call(self._up), + repeat=True, + enable_sound=changesound) + + bui.uicleanupcheck(self, self.nametext) + self._update_display() + + def _up(self) -> None: + self._value = min(self._maxval, self._value + self._increment) + self._changed() + + def _down(self) -> None: + self._value = max(self._minval, self._value - self._increment) + self._changed() + + def _changed(self) -> None: + self._update_display() + if self._callback: + self._callback(self._value) + babase.app.config[self._configkey[0]][self._configkey[1]][self._configkey[2]] = float(str(f'{self._value:.1f}')) + babase.app.config.apply_and_commit() + + def _update_display(self) -> None: + bui.textwidget(edit=self.valuetext, text=f'{self._value:.1f}') \ No newline at end of file diff --git a/plugins/utilities/disable_friendly_fire.py b/plugins/utilities/disable_friendly_fire.py new file mode 100644 index 0000000..0771cfe --- /dev/null +++ b/plugins/utilities/disable_friendly_fire.py @@ -0,0 +1,108 @@ +# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport) +# ba_meta require api 8 +from __future__ import annotations +from typing import TYPE_CHECKING + +import babase +import bauiv1 as bui +import bascenev1 as bs +import bascenev1lib +from bascenev1lib.gameutils import SharedObjects + +if TYPE_CHECKING: + pass + +class BombPickupMessage: + """ message says that someone pick up the dropped bomb """ + +# for bs.FreezeMessage +freeze: bool = True + +# ba_meta export plugin +class Plugin(babase.Plugin): + + # there are two ways to ignore our team player hits + # either change playerspaz handlemessage or change spaz handlemessage + def playerspaz_new_handlemessage(func: fuction) -> fuction: + def wrapper(*args, **kwargs): + global freeze + + # only run if session is dual team + if isinstance(args[0].activity.session, bs.DualTeamSession): + # when spaz got hurt by any reason this statement is runs. + if isinstance(args[1], bs.HitMessage): + our_team_players: list[type(args[0]._player)] + + # source_player + attacker = args[1].get_source_player(type(args[0]._player)) + + # our team payers + our_team_players = args[0]._player.team.players.copy() + + if len(our_team_players) > 0: + + # removing our self + our_team_players.remove(args[0]._player) + + # if we honding teammate or if we have a shield, do hit. + for player in our_team_players: + if player.actor.exists() and args[0]._player.actor.exists(): + if args[0]._player.actor.node.hold_node == player.actor.node or args[0]._player.actor.shield: + our_team_players.remove(player) + break + + if attacker in our_team_players: + freeze = False + return None + else: + freeze = True + + # if ice_bomb blast hits any spaz this statement runs. + elif isinstance(args[1], bs.FreezeMessage): + if not freeze: + freeze = True # use it and reset it + return None + + # orignal unchanged code goes here + func(*args, **kwargs) + + return wrapper + + # replace original fuction to modified function + bascenev1lib.actor.playerspaz.PlayerSpaz.handlemessage = playerspaz_new_handlemessage( + bascenev1lib.actor.playerspaz.PlayerSpaz.handlemessage) + + # let's add a message when bomb is pick by player + def bombfact_new_init(func: function) -> function: + def wrapper(*args): + + func(*args) # original code + + args[0].bomb_material.add_actions( + conditions=('they_have_material', SharedObjects.get().pickup_material), + actions=('message', 'our_node', 'at_connect', BombPickupMessage()), + ) + return wrapper + + # you get the idea + bascenev1lib.actor.bomb.BombFactory.__init__ = bombfact_new_init( + bascenev1lib.actor.bomb.BombFactory.__init__) + + def bomb_new_handlemessage(func: function) -> function: + def wrapper(*args, **kwargs): + # only run if session is dual team + if isinstance(args[0].activity.session, bs.DualTeamSession): + if isinstance(args[1], BombPickupMessage): + # get the pickuper and assign the pickuper to the source_player(attacker) of bomb blast + for player in args[0].activity.players: + if player.actor.exists(): + if player.actor.node.hold_node == args[0].node: + args[0]._source_player = player + break + + func(*args, **kwargs) # original + + return wrapper + + bascenev1lib.actor.bomb.Bomb.handlemessage = bomb_new_handlemessage( + bascenev1lib.actor.bomb.Bomb.handlemessage) \ No newline at end of file