diff --git a/dist/ba_root/mods/changelogs.json b/dist/ba_root/mods/changelogs.json index 547b5fb..f478784 100644 --- a/dist/ba_root/mods/changelogs.json +++ b/dist/ba_root/mods/changelogs.json @@ -34,6 +34,10 @@ "71":{ "log":"BS 1.7.10 update , bug fix , V2 account support, prop rotation", "time":"2 Oct 2022" + }, + "75": { + "log": "bug fixes, BS 1.7.19 , season end notfication, server restart notification, more maps and games", + "time": "4 March 2023" } } diff --git a/dist/ba_root/mods/custom_hooks.py b/dist/ba_root/mods/custom_hooks.py index aec8728..132a8c0 100644 --- a/dist/ba_root/mods/custom_hooks.py +++ b/dist/ba_root/mods/custom_hooks.py @@ -31,7 +31,7 @@ from spazmod import modifyspaz from tools import servercheck, ServerUpdate, logger, playlist from playersData import pdata from features import EndVote -from features import text_on_map +from features import text_on_map, announcement from features import map_fun from spazmod import modifyspaz if TYPE_CHECKING: @@ -194,9 +194,8 @@ def new_end(self, results: Any = None, delay: float = 0.0, force: bool = False): _ba.prop_axis(1, 0, 0) if isinstance(activity, CoopScoreScreen): team_balancer.checkToExitCoop() + announcement.showScoreScreenAnnouncement() org_end(self, results, delay, force) - - ba._activity.Activity.end = new_end org_player_join = ba._activity.Activity.on_player_join @@ -257,6 +256,7 @@ def shutdown(func) -> None: """Set the app to quit either now or at the next clean opportunity.""" def wrapper(*args, **kwargs): # add screen text and tell players we are going to restart soon. + ba.internal.chatmessage("Server will restart on next opportunity. (series end)") _ba.restart_scheduled = True _ba.get_foreground_host_activity().restart_msg = _ba.newnode('text', attrs={ diff --git a/dist/ba_root/mods/features/announcement.py b/dist/ba_root/mods/features/announcement.py new file mode 100644 index 0000000..5982c92 --- /dev/null +++ b/dist/ba_root/mods/features/announcement.py @@ -0,0 +1,12 @@ +import ba +import ba.internal + +import setting +import random +setti=setting.get_settings_data() + +def showScoreScreenAnnouncement(): + if setti["ScoreScreenAnnouncement"]["enable"]: + color=((0+random.random()*1.0),(0+random.random()*1.0),(0+random.random()*1.0)) + msgs = setti["ScoreScreenAnnouncement"]["msg"] + ba.screenmessage(random.choice(msgs), color = color) diff --git a/dist/ba_root/mods/features/text_on_map.py b/dist/ba_root/mods/features/text_on_map.py index 50615cc..a77296c 100644 --- a/dist/ba_root/mods/features/text_on_map.py +++ b/dist/ba_root/mods/features/text_on_map.py @@ -8,8 +8,6 @@ import ba.internal import setting from stats import mystats from datetime import datetime - - import random setti=setting.get_settings_data() class textonmap: @@ -30,7 +28,7 @@ class textonmap: self.nextGame(nextMap) self.restart_msg() if hasattr(_ba, "season_ends_in_days"): - if _ba.season_ends_in_days < 8: + if _ba.season_ends_in_days < 9: self.season_reset(_ba.season_ends_in_days) if setti["leaderboard"]["enable"]: self.leaderBoard() diff --git a/dist/ba_root/mods/games/BlockDash.py b/dist/ba_root/mods/games/BlockDash.py new file mode 100644 index 0000000..6e3a855 --- /dev/null +++ b/dist/ba_root/mods/games/BlockDash.py @@ -0,0 +1,482 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Elimination mini-game.""" + +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba,_ba +from bastd.actor.spazfactory import SpazFactory +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, Union +import random +from games.lib import Player,Team,Icon,eli +from bastd.game.elimination import EliminationGame + +# ba_meta export game +class BlockDashGame(EliminationGame): + """Game type where last player(s) left alive win.""" + + name = 'Block Dash' + description = 'Last remaining alive wins.' + scoreconfig = ba.ScoreConfig(label='Survived', + scoretype=ba.ScoreType.SECONDS, + none_is_winner=True) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + allow_mid_activity_joins = False + + + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ["Wooden Floor"] + + def __init__(self, settings: dict): + super().__init__(settings) + shared=SharedObjects.get() + self._scoreboard = Scoreboard() + self._start_time: Optional[float] = None + self._vs_text: Optional[ba.Actor] = None + self._round_end_timer: Optional[ba.Timer] = None + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player =1 + 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 = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.SURVIVAL) + self.laser_material=ba.Material() + self.laser_material.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=(('modify_part_collision', 'collide',True), + ('message','their_node','at_connect',ba.DieMessage())) + ) + + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Last team standing wins.' if isinstance( + self.session, ba.DualTeamSession) else 'Last one standing wins.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'last team standing wins' if isinstance( + self.session, ba.DualTeamSession) else 'last one standing wins' + + def on_player_join(self, player: Player) -> None: + 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 = ba.time() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + self.add_wall() + + if self._solo_mode: + self._vs_text = ba.NodeActor( + ba.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': ba.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, ba.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. + ba.timer(1.0, self._update, repeat=True) + + 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: + return + # lets do nothing ;Eat 5 Star + + def _get_spawn_point(self, player: Player) -> Optional[ba.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 = ba.Vec3(living_player_pos) + points: list[tuple[float, ba.Vec3]] = [] + for team in self.teams: + start_pos = ba.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) -> ba.Actor: + p=[-6,-4.3,-2.6,-0.9,0.8,2.5,4.2,5.9] + q=[-4,-2.3,-0.6,1.1,2.8,4.5] + + x=random.randrange(0,len(p)) + y=random.randrange(0,len(q)) + actor = self.spawn_player_spaz(player, position=(0,1.8,0)) + actor.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_pickup=False) + if not self._solo_mode: + ba.timer(0.3, ba.Call(self._print_lives, player)) + + # 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 bastd.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. + ba.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(ba.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, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + ba.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: + ba.playsound(SpazFactory.get().single_player_death_sound) + + # 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(ba.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) + + 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 = ba.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 = ba.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) + def add_wall(self): + # FIXME: Chop this into vr and non-vr chunks. + + shared = SharedObjects.get() + pwm=ba.Material() + cwwm=ba.Material() + # pwm.add_actions( + # actions=('modify_part_collision', 'friction', 0.0)) + # anything that needs to hit the wall should apply this. + + pwm.add_actions( + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + )) + self.mat = ba.Material() + self.mat.add_actions( + + actions=( ('modify_part_collision','physical',False), + ('modify_part_collision','collide',False)) + ) + + ud_1_r=ba.newnode('region',attrs={'position': (-2,0,-4),'scale': (14.5,1,14.5),'type': 'box','materials': [shared.footing_material,pwm ]}) + + node = ba.newnode('prop', + owner=ud_1_r, + attrs={ + 'model':ba.getmodel('image1x1'), + 'light_model':ba.getmodel('powerupSimple'), + 'position':(2,7,2), + 'body':'puck', + 'shadow_size':0.0, + 'velocity':(0,0,0), + 'color_texture':ba.gettexture('flagColor'), + 'model_scale':14.5, + 'reflection_scale':[1.5], + 'materials':[self.mat, shared.object_material,shared.footing_material], + }) + mnode = ba.newnode('math', + owner=ud_1_r, + attrs={ + 'input1': (0, 0.7, 0), + 'operation': 'add' + }) + + node.changerotation(1,0,0) + + ud_1_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', node, 'position') + ba.timer(8,ba.Call(self.create_block_wall_easy)) + self.gate_count=4 + self.wall_count=0 + + def create_wall(self): + x=-9 + for i in range(0,17): + self.create_block(x,0.5) + self.create_block(x,1.2) + x=x+0.85 + + def create_block_wall_hardest(self): + x=-3 + + for i in range(0,7): + self.create_block(x,0.4) + x=x+0.85 + ba.timer(1.5,ba.Call(self.create_wall)) + ba.timer(15,ba.Call(self.create_block_wall_hardest)) + + def create_block_wall_hard(self): + x=-9 + self.wall_count+=1 + for i in range(0,17): + self.create_block(x,0.4) + x=x+0.85 + if self.wall_count <4: + ba.timer(12,ba.Call(self.create_block_wall_hard)) + else: + ba.timer(7,ba.Call(self.create_block_wall_hard)) #hardest too heavy to play + + + def create_block_wall_easy(self): + x=-9 + c=0 + for i in range(0,17): + if random.randrange(0,2) and c ba.Actor: + super().spawn_player(player) + auto_stunt.spawn_mirror_spaz(player) diff --git a/dist/ba_root/mods/games/Collector.py b/dist/ba_root/mods/games/Collector.py new file mode 100644 index 0000000..1a22bda --- /dev/null +++ b/dist/ba_root/mods/games/Collector.py @@ -0,0 +1,642 @@ +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +''' + Gamemode: Collector + Creator: TheMikirog + Website: https://bombsquadjoyride.blogspot.com/ + + This is a gamemode purely made by me just to spite unchallenged modders + out there that put out crap to the market. + We don't want gamemodes that are just the existing ones + with some novelties! Gamers deserve more! + + In this gamemode you have to kill others in order to get their Capsules. + Capsules can be collected and staked in your inventory, + how many as you please. + After you kill an enemy that carries some of them, + they drop a respective amount of Capsules they carried + two more. + Your task is to collect these Capsules, + get to the flag and score them KOTH style. + You can't score if you don't have any Capsules with you. + The first player or team to get to the required ammount wins. + This is a gamemode all about trying to stay alive + and picking your battles in order to win. + A rare skill in BombSquad, where everyone is overly aggressive. +''' + +from __future__ import annotations + +import weakref +from enum import Enum +from typing import TYPE_CHECKING + +import ba +import random +from bastd.actor.flag import Flag +from bastd.actor.popuptext import PopupText +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = ba.app.lang.language +if lang == 'Spanish': + name = 'Coleccionista' + description = ('Elimina a tus oponentes para robar sus cápsulas.\n' + '¡Recolecta y anota en el punto de depósito!') + description_ingame = 'Obtén ${ARG1} cápsulas de tus enemigos.' + description_short = 'colecciona ${ARG1} cápsulas' + tips = [( + '¡Si tu oponente cae fuera del mapa, sus cápsulas desapareceran!\n' + 'No intestes matar a tus enemigos arrojándolos al vacio.'), + 'No te apresures. ¡Puedes perder tus cápsulas rápidamente!', + ('¡No dejes que el jugador con más cápsulas anote!\n' + '¡Intenta atraparlo si puedes!'), + ('¡Las Capsulas de la Suerte te dan 4 cápsulas en lugar de 2' + 'y tienen un 8% de probabilidad de aparecer después de matar'), + ('¡No te quedes en un solo lugar! Muevete más rapido que tu enemigo, ' + '¡con suerte conseguirás algunas cápsulas!'), + ] + capsules_to_win = 'Cápsulas para Ganar' + capsules_death = 'Cápsulas al Morir' + lucky_capsules = 'Cápsulas de la Suerte' + bonus = '¡BONUS!' + full_capacity = '¡Capacidad Completa!' +else: + name = 'Collector' + description = ('Kill your opponents to steal their Capsules.\n' + 'Collect them and score at the Deposit point!') + description_ingame = 'Score ${ARG1} capsules from your enemies.' + description_short = 'collect ${ARG1} capsules' + tips = [( + 'Making you opponent fall down the pit makes his Capsules wasted!\n' + 'Try not to kill enemies by throwing them off the cliff.'), + 'Don\'t be too reckless. You can lose your loot quite quickly!', + ('Don\'t let the leading player score his Capsules ' + 'at the Deposit Point!\nTry to catch him if you can!'), + ('Lucky Capsules give 4 to your inventory and they have 8% chance ' + 'of spawning after kill!'), + ('Don\'t camp in one place! Make your move first, ' + 'so hopefully you get some dough!'), + ] + capsules_to_win = 'Capsules to Win' + capsules_death = 'Capsules on Death' + lucky_capsules = 'Allow Lucky Capsules' + bonus = 'BONUS!' + full_capacity = 'Full Capacity!' + + +class FlagState(Enum): + """States our single flag can be in.""" + + NEW = 0 + UNCONTESTED = 1 + CONTESTED = 2 + HELD = 3 + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.time_at_flag = 0 + self.capsules = 0 + self.light = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class CollectorGame(ba.TeamGameActivity[Player, Team]): + + name = name + description = description + tips = tips + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[ba.Session] + ) -> list[ba.Setting]: + settings = [ + ba.IntSetting( + capsules_to_win, + min_value=1, + default=10, + increment=1, + ), + ba.IntSetting( + capsules_death, + min_value=1, + max_value=10, + default=2, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting(lucky_capsules, default=True), + ba.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) or issubclass( + sessiontype, ba.FreeForAllSession + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ba.getmaps('keep_away') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._score_to_win: int | None = None + self._swipsound = ba.getsound('swip') + self._lucky_sound = ba.getsound('ding') + + self._flag_pos: Sequence[float] | None = None + self._flag_state: FlagState | None = None + self._flag: Flag | None = None + self._flag_light: ba.Node | None = None + self._scoring_team: weakref.ref[Team] | None = None + self._time_limit = float(settings['Time Limit']) + self._epic_mode = bool(settings['Epic Mode']) + + self._capsules_to_win = int(settings[capsules_to_win]) + self._capsules_death = int(settings[capsules_death]) + self._lucky_capsules = bool(settings[lucky_capsules]) + self._capsules: list[Any] = [] + + self._capsule_model = ba.getmodel('bomb') + self._capsule_tex = ba.gettexture('bombColor') + self._capsule_lucky_tex = ba.gettexture('bombStickyColor') + self._collect_sound = ba.getsound('powerup01') + self._lucky_collect_sound = ba.getsound('cashRegister2') + + self._capsule_material = ba.Material() + self._capsule_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=('call', 'at_connect', self._on_capsule_player_collide), + ) + + self._flag_region_material = ba.Material() + self._flag_region_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ( + 'call', + 'at_connect', + ba.Call(self._handle_player_flag_region_collide, True), + ), + ( + 'call', + 'at_disconnect', + ba.Call(self._handle_player_flag_region_collide, False), + ), + ), + ) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = ( + ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SCARY + ) + + def get_instance_description(self) -> str | Sequence: + return description_ingame, self._score_to_win + + def get_instance_description_short(self) -> str | Sequence: + return description_short, self._score_to_win + + def create_team(self, sessionteam: ba.SessionTeam) -> Team: + return Team() + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + shared = SharedObjects.get() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = self._capsules_to_win * max( + 1, max(len(t.players) for t in self.teams) + ) + self._update_scoreboard() + + if isinstance(self.session, ba.FreeForAllSession): + self._flag_pos = self.map.get_flag_position(random.randint(0, 1)) + else: + self._flag_pos = self.map.get_flag_position(None) + + ba.timer(1.0, self._tick, repeat=True) + self._flag_state = FlagState.NEW + Flag.project_stand(self._flag_pos) + self._flag = Flag( + position=self._flag_pos, touchable=False, color=(1, 1, 1) + ) + self._flag_light = ba.newnode( + 'light', + attrs={ + 'position': self._flag_pos, + 'intensity': 0.2, + 'height_attenuated': False, + 'radius': 0.4, + 'color': (0.2, 0.2, 0.2), + }, + ) + # Flag region. + flagmats = [self._flag_region_material, shared.region_material] + ba.newnode( + 'region', + attrs={ + 'position': self._flag_pos, + 'scale': (1.8, 1.8, 1.8), + 'type': 'sphere', + 'materials': flagmats, + }, + ) + self._update_flag_state() + + def _tick(self) -> None: + self._update_flag_state() + + if self._scoring_team is None: + scoring_team = None + else: + scoring_team = self._scoring_team() + + if not scoring_team: + return + + if isinstance(self.session, ba.FreeForAllSession): + players = self.players + else: + players = scoring_team.players + + for player in players: + if player.time_at_flag > 0: + self.stats.player_scored( + player, 3, screenmessage=False, display=False + ) + if player.capsules > 0: + if self._flag_state != FlagState.HELD: + return + if scoring_team.score >= self._score_to_win: + return + + player.capsules -= 1 + scoring_team.score += 1 + self._handle_capsule_storage(( + self._flag_pos[0], + self._flag_pos[1]+1, + self._flag_pos[2] + ), player) + ba.playsound( + self._collect_sound, + 0.8, + position=self._flag_pos) + + self._update_scoreboard() + if player.capsules > 0: + assert self._flag is not None + self._flag.set_score_text( + str(self._score_to_win - scoring_team.score)) + + # winner + if scoring_team.score >= self._score_to_win: + self.end_game() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results, announce_delay=0) + + def _update_flag_state(self) -> None: + holding_teams = set( + player.team for player in self.players if player.time_at_flag + ) + prev_state = self._flag_state + assert self._flag_light + assert self._flag is not None + assert self._flag.node + if len(holding_teams) > 1: + self._flag_state = FlagState.CONTESTED + self._scoring_team = None + self._flag_light.color = (0.6, 0.6, 0.1) + self._flag.node.color = (1.0, 1.0, 0.4) + elif len(holding_teams) == 1: + holding_team = list(holding_teams)[0] + self._flag_state = FlagState.HELD + self._scoring_team = weakref.ref(holding_team) + self._flag_light.color = ba.normalized_color(holding_team.color) + self._flag.node.color = holding_team.color + else: + self._flag_state = FlagState.UNCONTESTED + self._scoring_team = None + self._flag_light.color = (0.2, 0.2, 0.2) + self._flag.node.color = (1, 1, 1) + if self._flag_state != prev_state: + ba.playsound(self._swipsound) + + def _handle_player_flag_region_collide(self, colliding: bool) -> None: + try: + spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True) + except ba.NotFoundError: + return + + if not spaz.is_alive(): + return + + player = spaz.getplayer(Player, True) + + # Different parts of us can collide so a single value isn't enough + # also don't count it if we're dead (flying heads shouldn't be able to + # win the game :-) + if colliding and player.is_alive(): + player.time_at_flag += 1 + else: + player.time_at_flag = max(0, player.time_at_flag - 1) + + self._update_flag_state() + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value( + team, team.score, self._score_to_win + ) + + def _drop_capsule(self, player: Player) -> None: + pt = player.node.position + + # Throw out capsules that the victim has + 2 more to keep the game running + for i in range(player.capsules + self._capsules_death): + # How far from each other these capsules should spawn + w = 0.6 + # How much these capsules should fly after spawning + s = 0.005 - (player.capsules * 0.01) + self._capsules.append( + Capsule( + position=(pt[0] + random.uniform(-w, w), + pt[1] + 0.75 + random.uniform(-w, w), + pt[2]), + velocity=(random.uniform(-s, s), + random.uniform(-s, s), + random.uniform(-s, s)), + lucky=False)) + if random.randint(1, 12) == 1 and self._lucky_capsules: + # How far from each other these capsules should spawn + w = 0.6 + # How much these capsules should fly after spawning + s = 0.005 + self._capsules.append( + Capsule( + position=(pt[0] + random.uniform(-w, w), + pt[1] + 0.75 + random.uniform(-w, w), + pt[2]), + velocity=(random.uniform(-s, s), + random.uniform(-s, s), + random.uniform(-s, s)), + lucky=True)) + + def _on_capsule_player_collide(self) -> None: + if self.has_ended(): + return + collision = ba.getcollision() + + # Be defensive here; we could be hitting the corpse of a player + # who just left/etc. + try: + capsule = collision.sourcenode.getdelegate(Capsule, True) + player = collision.opposingnode.getdelegate( + PlayerSpaz, True + ).getplayer(Player, True) + except ba.NotFoundError: + return + + if not player.is_alive(): + return + + if capsule.node.color_texture == self._capsule_lucky_tex: + player.capsules += 4 + PopupText( + bonus, + color=(1, 1, 0), + scale=1.5, + position=capsule.node.position + ).autoretain() + ba.playsound( + self._lucky_collect_sound, + 1.0, + position=capsule.node.position) + ba.emitfx( + position=capsule.node.position, + velocity=(0, 0, 0), + count=int(6.4+random.random()*24), + scale=1.2, + spread=2.0, + chunk_type='spark'); + ba.emitfx( + position=capsule.node.position, + velocity=(0, 0, 0), + count=int(4.0+random.random()*6), + emit_type='tendrils'); + else: + player.capsules += 1 + ba.playsound( + self._collect_sound, + 0.6, + position=capsule.node.position) + # create a flash + light = ba.newnode( + 'light', + attrs={ + 'position': capsule.node.position, + 'height_attenuated': False, + 'radius': 0.1, + 'color': (1, 1, 0)}) + + # Create a short text informing about your inventory + self._handle_capsule_storage(player.position, player) + + ba.animate(light, 'intensity', { + 0: 0, + 0.1: 0.5, + 0.2: 0 + }, loop=False) + ba.timer(0.2, light.delete) + capsule.handlemessage(ba.DieMessage()) + + def _update_player_light(self, player: Player, capsules: int) -> None: + if player.light: + intensity = 0.04 * capsules + ba.animate(player.light, 'intensity', { + 0.0: player.light.intensity, + 0.1: intensity + }) + def newintensity(): + player.light.intensity = intensity + ba.timer(0.1, newintensity) + else: + player.light = ba.newnode( + 'light', + attrs={ + 'height_attenuated': False, + 'radius': 0.2, + 'intensity': 0.0, + 'color': (0.2, 1, 0.2) + }) + player.node.connectattr('position', player.light, 'position') + + def _handle_capsule_storage(self, pos: float, player: Player) -> None: + capsules = player.capsules + text = str(capsules) + scale = 1.75 + (0.02 * capsules) + if capsules > 10: + player.capsules = 10 + text = full_capacity + color = (1, 0.85, 0) + elif capsules > 7: + color = (1, 0, 0) + scale = 2.4 + elif capsules > 5: + color = (1, 0.4, 0.4) + scale = 2.1 + elif capsules > 3: + color = (1, 1, 0.4) + scale = 2.0 + else: + color = (1, 1, 1) + scale = 1.9 + PopupText( + text, + color=color, + scale=scale, + position=(pos[0], pos[1]-1, pos[2]) + ).autoretain() + self._update_player_light(player, capsules) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) # Augment default. + # No longer can count as time_at_flag once dead. + player = msg.getplayer(Player) + player.time_at_flag = 0 + self._update_flag_state() + self._drop_capsule(player) + player.capsules = 0 + self._update_player_light(player, 0) + self.respawn_player(player) + else: + return super().handlemessage(msg) + + +class Capsule(ba.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.5, 0.0), + lucky: bool = False): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # spawn just above the provided point + self._spawn_pos = (position[0], position[1], position[2]) + + if lucky: + ba.playsound(activity._lucky_sound, 1.0, self._spawn_pos) + + self.node = ba.newnode( + 'prop', + attrs={ + 'model': activity._capsule_model, + 'color_texture': activity._capsule_lucky_tex if lucky else ( + activity._capsule_tex), + 'body': 'crate' if lucky else 'capsule', + 'reflection': 'powerup' if lucky else 'soft', + 'body_scale': 0.65 if lucky else 0.3, + 'density':6.0 if lucky else 4.0, + 'reflection_scale': [0.15], + 'shadow_size': 0.65 if lucky else 0.6, + 'position': self._spawn_pos, + 'velocity': velocity, + 'materials': [ + shared.object_material, activity._capsule_material] + }, + delegate=self) + ba.animate(self.node, 'model_scale', { + 0.0: 0.0, + 0.1: 0.9 if lucky else 0.6, + 0.16: 0.8 if lucky else 0.5 + }) + self._light_capsule = ba.newnode( + 'light', + attrs={ + 'position': self._spawn_pos, + 'height_attenuated': False, + 'radius': 0.5 if lucky else 0.1, + 'color': (0.2, 0.2, 0) if lucky else (0.2, 1, 0.2) + }) + self.node.connectattr('position', self._light_capsule, 'position') + + def handlemessage(self, msg: Any): + if isinstance(msg, ba.DieMessage): + self.node.delete() + ba.animate(self._light_capsule, 'intensity', { + 0: 1.0, + 0.05: 0.0 + }, loop=False) + ba.timer(0.05, self._light_capsule.delete) + elif isinstance(msg, ba.OutOfBoundsMessage): + self.handlemessage(ba.DieMessage()) + elif isinstance(msg, ba.HitMessage): + self.node.handlemessage( + 'impulse', + msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0]/8, msg.velocity[1]/8, msg.velocity[2]/8, + 1.0*msg.magnitude, 1.0*msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + else: + return super().handlemessage(msg) diff --git a/dist/ba_root/mods/games/FlagDay.py b/dist/ba_root/mods/games/FlagDay.py new file mode 100644 index 0000000..48ed26c --- /dev/null +++ b/dist/ba_root/mods/games/FlagDay.py @@ -0,0 +1,610 @@ +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba +import json +import math +import random +from bastd.game.elimination import Icon +from bastd.actor.bomb import Bomb, Blast +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBox +from bastd.actor.flag import Flag, FlagPickedUpMessage +from bastd.actor.spazbot import SpazBotSet, BrawlerBotLite, SpazBotDiedMessage + +if TYPE_CHECKING: + from typing import Any, Sequence + + +lang = ba.app.lang.language +if lang == 'Spanish': + name = 'Día de la Bandera' + description = ('Recoge las banderas para recibir un premio.\n' + 'Pero ten cuidado...') + slow_motion_deaths = 'Muertes en Cámara Lenta' + credits = 'Creado por MattZ45986 en Github | Actualizado por byANG3L' + you_were = 'Estas' + cursed_text = 'MALDITO' + run = 'CORRE' + climb_top = 'Escala a la cima' + bomb_rain = '¡LLUVIA DE BOMBAS!' + lame_guys = 'Chicos Ligeros' + jackpot = '¡PREMIO MAYOR!' + diedtxt = '¡' + diedtxt2 = ' ha sido eliminado!' +else: + name = 'Flag Day' + description = 'Pick up flags to receive a prize.\nBut beware...' + slow_motion_deaths = 'Slow Motion Deaths' + credits = 'Created by MattZ45986 on Github | Updated by byANG3L' + you_were = 'You were' + cursed_text = 'CURSED' + run = 'RUN' + climb_top = 'Climb to the top' + bomb_rain = 'BOMB RAIN!' + lame_guys = 'Lame Guys' + jackpot = '!JACKPOT!' + diedtxt = '' + diedtxt2 = ' died!' + + +class Icon(Icon): + + 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, + dead: bool = False, + ): + super().__init__(player,position,scale,show_lives,show_death, + name_scale,name_maxwidth,flatness,shadow) + if dead: + self._name_text.opacity = 0.2 + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + +class FlagBearer(PlayerSpaz): + def handlemessage(self, msg: Any) -> Any: + super().handlemessage(msg) + if isinstance(msg, ba.PowerupMessage): + activity = self.activity + player = self.getplayer(Player) + if not player.is_alive(): + return + if activity.last_prize == 'curse': + player.team.score += 25 + activity._update_scoreboard() + elif activity.last_prize == 'land_mines': + player.team.score += 15 + activity._update_scoreboard() + self.connect_controls_to_player() + elif activity.last_prize == 'climb': + player.team.score += 50 + activity._update_scoreboard() + if msg.poweruptype == 'health': + activity.round_timer = None + ba.timer(0.2, activity.setup_next_round) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.dead = False + self.icons: list[Icon] = [] + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class FlagDayGame(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = name + description = description + + # Print messages when players die since it matters here. + announce_player_deaths = True + + allow_mid_activity_joins = False + + @classmethod + def get_available_settings( + cls, sessiontype: type[ba.Session] + ) -> list[ba.Setting]: + settings = [ + ba.BoolSetting(slow_motion_deaths, default=True), + ba.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return ( + issubclass(sessiontype, ba.CoopSession) + or issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession) + ) + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ['Courtyard'] + + def __init__(self, settings: dict): + super().__init__(settings) + self.credits() + self._scoreboard = Scoreboard() + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._slow_motion_deaths = bool(settings[slow_motion_deaths]) + self.current_player: Player | None = None + self.prize_recipient: Player | None = None + self.bomb_survivor: Player | None = None + self.bad_guy_cost: int = 0 + self.player_index: int = 0 + self.bombs: list = [] + self.queue_line: list = [] + self._bots: SpazBotSet | None = None + self.light: ba.Node | None = None + self.last_prize = 'none' + self._flag: Flag | None = None + self._flag2: Flag | None = None + self._flag3: Flag | None = None + self._flag4: Flag | None = None + self._flag5: Flag | None = None + self._flag6: Flag | None = None + self._flag7: Flag | None = None + self._flag8: Flag | None = None + self.set = False + self.round_timer: ba.Timer | None = None + self.give_points_timer: ba.Timer | None = None + + self._jackpot_sound = ba.getsound('achievement') + self._round_sound = ba.getsound('powerup01') + self._dingsound = ba.getsound('dingSmall') + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = ( + ba.MusicType.EPIC if self._epic_mode else ba.MusicType.TO_THE_DEATH + ) + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + if player is self.current_player: + self.setup_next_round() + self._check_end_game() + super().on_player_leave(player) + self.queue_line.remove(player) + self._update_icons() + + def on_begin(self) -> None: + super().on_begin() + for player in self.players: + if player.actor: + player.actor.handlemessage(ba.DieMessage()) + player.actor.node.delete() + self.queue_line.append(player) + self.spawn_player_spaz( + self.queue_line[self.player_index % len(self.queue_line)], + (0.0, 3.0, -2.0)) + self.current_player = self.queue_line[0] + # Declare a set of bots (enemies) that we will use later + self._bots = SpazBotSet() + self.reset_flags() + self._update_icons() + self._update_scoreboard() + + def credits(self) -> None: + ba.newnode( + 'text', + attrs={ + 'v_attach': 'bottom', + 'h_align': 'center', + 'vr_depth': 0, + 'color': (0, 0.2, 0), + 'shadow': 1.0, + 'flatness': 1.0, + 'position': (0,0), + 'scale': 0.8, + 'text': credits + }) + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + for player in self.queue_line: + player.icons = [] + if player == self.current_player: + xval = 0 + x_offs = -78 + player.icons.append( + Icon(player, + position=(xval, 65), + scale=1.0, + name_maxwidth=130, + name_scale=0.8, + flatness=0.0, + shadow=0.5, + show_death=True, + show_lives=False)) + elif player.dead: + xval = 65 + x_offs = 78 + player.icons.append( + Icon(player, + position=(xval, 50), + scale=0.5, + name_maxwidth=75, + name_scale=1.0, + flatness=1.0, + shadow=1.0, + show_death=False, + show_lives=False, + dead=True)) + xval += x_offs * 0.56 + else: + xval = -65 + x_offs = 78 + player.icons.append( + Icon(player, + position=(xval, 50), + scale=0.5, + name_maxwidth=75, + name_scale=1.0, + flatness=1.0, + shadow=1.0, + show_death=False, + show_lives=False)) + xval -= x_offs * 0.56 + + def give_prize(self, prize: int) -> None: + if prize == 1: + # Curse him aka make him blow up in 5 seconds + # give them a nice message + ba.screenmessage(you_were, color=(0.1, 0.1, 0.1)) + ba.screenmessage(cursed_text, color=(1.0, 0.0, 0.0)) + self.make_health_box((0.0, 0.0, 0.0)) + self.last_prize = 'curse' + self.prize_recipient.actor.curse() + # ba.timer(5.5, self.setup_next_round) + if prize == 2: + self.setup_rof() + ba.screenmessage(run, color=(1.0, 0.2, 0.1)) + self.last_prize = 'ring_of_fire' + if prize == 3: + self.last_prize = 'climb' + self.light = ba.newnode( + 'locator', + attrs={ + 'shape': 'circle', + 'position': (0.0, 3.0, -9.0), + 'color': (1.0, 1.0, 1.0), + 'opacity': 1.0, + 'draw_beauty': True, + 'additive': True + }) + ba.screenmessage(climb_top, color=(0.5, 0.5, 0.5)) + ba.timer(3.0, ba.Call(self.make_health_box, (0.0, 6.0, -9.0))) + self.round_timer = ba.Timer(10.0, self.setup_next_round) + if prize == 4: + self.last_prize = 'land_mines' + self.make_health_box((6.0, 5.0, -2.0)) + self.make_land_mines() + self.prize_recipient.actor.connect_controls_to_player( + enable_bomb=False) + self.prize_recipient.actor.node.handlemessage( + ba.StandMessage(position=(-6.0, 3.0, -2.0))) + self.round_timer = ba.Timer(7.0, self.setup_next_round) + if prize == 5: + # Make it rain bombs + self.bomb_survivor = self.prize_recipient + ba.screenmessage(bomb_rain, color=(1.0, 0.5, 0.16)) + # Set positions for the bombs to drop + for bzz in range(-5,6): + for azz in range(-5,2): + # for each position make a bomb drop there + self.make_bomb(bzz, azz) + self.give_points_timer = ba.Timer(3.3, self.give_points) + self.last_prize = 'bombrain' + if prize == 6: + self.setup_br() + self.bomb_survivor = self.prize_recipient + self.give_points_timer = ba.Timer(7.0, self.give_points) + self.last_prize = 'bombroad' + if prize == 7: + # makes killing a bad guy worth ten points + self.bad_guy_cost = 2 + ba.screenmessage(lame_guys, color=(1.0, 0.5, 0.16)) + # makes a set of nine positions + for a in range(-1, 2): + for b in range(-3, 0): + # and spawns one in each position + self._bots.spawn_bot(BrawlerBotLite, pos=(a, 2.5, b)) + # and we give our player boxing gloves and a shield + self._player.equip_boxing_gloves() + self._player.equip_shields() + self.last_prize = 'lameguys' + if prize == 8: + ba.playsound(self._jackpot_sound) + ba.screenmessage(jackpot, color=(1.0, 0.0, 0.0)) + ba.screenmessage(jackpot, color=(0.0, 1.0, 0.0)) + ba.screenmessage(jackpot, color=(0.0, 0.0, 1.0)) + team = self.prize_recipient.team + # GIVE THEM A WHOPPING 50 POINTS!!! + team.score += 50 + # and update the scores + self._update_scoreboard() + self.last_prize = 'jackpot' + ba.timer(2.0, self.setup_next_round) + + def setup_next_round(self) -> None: + if self._slow_motion_deaths: + ba.getactivity().globalsnode.slow_motion = False + if self.set: + return + if self.light: + self.light.delete() + for bomb in self.bombs: + bomb.handlemessage(ba.DieMessage()) + self.kill_flags() + self._bots.clear() + self.reset_flags() + self.current_player.actor.handlemessage( + ba.DieMessage(how='game')) + self.current_player.actor.node.delete() + c = 0 + self.player_index += 1 + self.player_index %= len(self.queue_line) + if len(self.queue_line) > 0: + while self.queue_line[self.player_index].dead: + if c > len(self.queue_line): + return + self.player_index += 1 + self.player_index %= len(self.queue_line) + c += 1 + self.spawn_player_spaz( + self.queue_line[self.player_index], (0.0, 3.0, -2.0)) + self.current_player = self.queue_line[self.player_index] + self.last_prize = 'none' + self._update_icons() + + def check_bots(self) -> None: + if not self._bots.have_living_bots(): + self.setup_next_round() + + def make_land_mines(self) -> None: + self.bombs = [] + for i in range(-11, 7): + self.bombs.append(Bomb( + position=(0.0, 6.0, i/2.0), + bomb_type='land_mine', + blast_radius=2.0)) + self.bombs[i+10].arm() + + def give_points(self) -> None: + if self.bomb_survivor is not None and self.bomb_survivor.is_alive(): + self.bomb_survivor.team.score += 20 + self._update_scoreboard() + self.round_timer = ba.Timer(1.0, self.setup_next_round) + + def make_health_box(self, position: Sequence[float]) -> None: + if position == (0.0, 3.0, 0.0): + position = (random.randint(-6, 6), 6, random.randint(-6, 4)) + elif position == (0,0,0): + position = random.choice( + ((-7, 6, -5), (7, 6, -5), (-7, 6, 1), (7, 6, 1))) + self.health_box = PowerupBox( + position=position, poweruptype='health').autoretain() + + # called in prize #5 + def make_bomb(self, xpos: float, zpos: float) -> None: + # makes a bomb at the given position then auto-retains it aka: + # makes sure it doesn't disappear because there is no reference to it + self.bombs.append(Bomb(position=(xpos, 12, zpos))) + + def setup_br(self) -> None: + self.make_bomb_row(6) + self.prize_recipient.actor.handlemessage( + ba.StandMessage(position=(6.0, 3.0, -2.0))) + + def make_bomb_row(self, num: int) -> None: + if not self.prize_recipient.is_alive(): + return + if num == 0: + self.round_timer = ba.Timer(1.0, self.setup_next_round) + return + for i in range(-11, 7): + self.bombs.append( + Bomb(position=(-3, 3, i/2.0), + velocity=(12, 0.0, 0.0), + bomb_type='normal', + blast_radius=1.2)) + ba.timer(1.0, ba.Call(self.make_bomb_row, num-1)) + + def setup_rof(self) -> None: + self.make_blast_ring(10) + self.prize_recipient.actor.handlemessage( + ba.StandMessage(position=(0.0, 3.0, -2.0))) + + def make_blast_ring(self, length: float) -> None: + if not self.prize_recipient.is_alive(): + return + if length == 0: + self.setup_next_round() + self.prize_recipient.team.score += 50 + self._update_scoreboard() + return + for angle in range(0, 360, 45): + angle += random.randint(0, 45) + angle %= 360 + x = length * math.cos(math.radians(angle)) + z = length * math.sin(math.radians(angle)) + blast = Blast(position=(x, 2.2, z-2), blast_radius=3.5) + ba.timer(0.75, ba.Call(self.make_blast_ring, length-1)) + + # a method to remake the flags + def reset_flags(self) -> None: + # remake the flags + self._flag = Flag( + position=(0.0, 3.0, 1.0), touchable=True, color=(0.0, 0.0, 1.0)) + self._flag2 = Flag( + position=(0.0, 3.0, -5.0), touchable=True, color=(1.0, 0.0, 0.0)) + self._flag3 = Flag( + position=(3.0, 3.0, -2.0), touchable=True, color=(0.0, 1.0, 0.0)) + self._flag4 = Flag( + position=(-3.0, 3.0, -2.0), touchable=True, color=(1.0, 1.0, 1.0)) + self._flag5 = Flag( + position=(1.8, 3.0, 0.2), touchable=True, color=(0.0, 1.0, 1.0)) + self._flag6 = Flag( + position=(-1.8, 3.0, 0.2), touchable=True, color=(1.0, 0.0, 1.0)) + self._flag7 = Flag( + position=(1.8, 3.0, -3.8), touchable=True, color=(1.0, 1.0, 0.0)) + self._flag8 = Flag( + position=(-1.8, 3.0, -3.8), touchable=True, color=(0.0, 0.0, 0.0)) + + # a method to kill the flags + def kill_flags(self) -> None: + # destroy all the flags by erasing all references to them, + # indicated by None similar to null + self._flag.node.delete() + self._flag2.node.delete() + self._flag3.node.delete() + self._flag4.node.delete() + self._flag5.node.delete() # 132, 210 ,12 + self._flag6.node.delete() + self._flag7.node.delete() + self._flag8.node.delete() + + def _check_end_game(self) -> None: + for player in self.queue_line: + if not player.dead: + return + self.end_game() + + def spawn_player_spaz( + self, + player: PlayerType, + position: Sequence[float] = (0, 0, 0), + angle: float | None = None, + ) -> PlayerSpaz: + from ba import _math + from ba._gameutils import animate + from ba._coopsession import CoopSession + + angle = None + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = ba.safecolor(color, target_intensity=0.75) + + spaz = FlagBearer(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + ba.StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + ba.playsound(self._spawn_sound, 1, position=spaz.node.position) + light = ba.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + ba.timer(0.5, light.delete) + return spaz + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # give them a nice farewell + if ba.time() < 0.5: + return + if msg.how == 'game': + return + player = msg.getplayer(Player) + ba.screenmessage( + diedtxt + str(player.getname()) + diedtxt2, color=player.color) + player.dead = True + if player is self.current_player: + self.round_timer = None + self.give_points_timer = None + if not msg.how is ba.DeathType.FALL: + if self._slow_motion_deaths: + ba.getactivity().globalsnode.slow_motion = True + time = 0.5 + else: + time = 0.01 + # check to see if we can end the game + self._check_end_game() + ba.timer(time, self.setup_next_round) + elif isinstance(msg, FlagPickedUpMessage): + msg.flag.last_player_to_hold = msg.node.getdelegate( + FlagBearer, True + ).getplayer(Player, True) + self._player = msg.node.getdelegate( + FlagBearer, True + ) + self.prize_recipient = msg.node.getdelegate( + FlagBearer, True + ).getplayer(Player, True) + self.kill_flags() + self.give_prize(random.randint(1, 8)) + ba.playsound(self._round_sound) + self.current_player = self.prize_recipient + elif isinstance(msg, SpazBotDiedMessage): + # find out which team the last person to hold a flag was on + team = self.prize_recipient.team + # give them their points + team.score += self.bad_guy_cost + ba.playsound(self._dingsound, 0.5) + # update the scores + for team in self.teams: + self._scoreboard.set_team_value(team, team.score) + ba.timer(0.3, self.check_bots) + return None + + def _update_scoreboard(self) -> None: + for player in self.queue_line: + if not player.dead: + if player.team.score > 0: + ba.playsound(self._dingsound) + self._scoreboard.set_team_value(player.team, player.team.score) + + def end_game(self) -> None: + if self.set: + return + self.set = True + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/dist/ba_root/mods/games/soccer.so b/dist/ba_root/mods/games/RealSoccer.so similarity index 100% rename from dist/ba_root/mods/games/soccer.so rename to dist/ba_root/mods/games/RealSoccer.so diff --git a/dist/ba_root/mods/games/air_soccer.py b/dist/ba_root/mods/games/air_soccer.py new file mode 100644 index 0000000..aef11cd --- /dev/null +++ b/dist/ba_root/mods/games/air_soccer.py @@ -0,0 +1,528 @@ +# Released under the MIT License. See LICENSE for details. +# BY Stary_Agent +"""Hockey game and support classes.""" + +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba,_ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.gameutils import SharedObjects + +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 + +def create_slope(self): + shared = SharedObjects.get() + x=5 + y=12 + for i in range(0,10): + ba.newnode('region',attrs={'position': (x, y, -5.52),'scale': (0.2,0.1,6),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + x= x+0.3 + y=y+0.1 + +class Puck(ba.Actor): + """A lovely giant hockey puck.""" + + def __init__(self, position: Sequence[float] = (0.0, 13.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: Dict[int, Player] = {} + self.scored = False + assert activity is not None + assert isinstance(activity, HockeyGame) + pmats = [shared.object_material, activity.puck_material] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': activity.puck_model, + 'color_texture': activity.puck_tex, + 'body': 'sphere', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'gravity_scale':0.3, + 'shadow_size': 0.5, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + ba.animate(self.node, 'model_scale', {0: 0, 0.2: 1.3, 0.26: 1}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.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, ba.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, ba.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.team.id] = s_player + else: + super().handlemessage(msg) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class AirSoccerGame(ba.TeamGameActivity[Player, Team]): + """Ice hockey game.""" + + name = 'Epic Air Soccer' + description = 'Score some goals.' + available_settings = [ + ba.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.1), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ] + default_music = ba.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Creative Thoughts'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self.slow_motion = True + self._scoreboard = Scoreboard() + self._cheer_sound = ba.getsound('cheer') + self._chant_sound = ba.getsound('crowdChant') + self._foghorn_sound = ba.getsound('foghorn') + self._swipsound = ba.getsound('swip') + self._whistle_sound = ba.getsound('refWhistle') + self.puck_model = ba.getmodel('bomb') + self.puck_tex = ba.gettexture('landMine') + self.puck_scored_tex = ba.gettexture('landMineLit') + self._puck_sound = ba.getsound('metalHit') + self.puck_material = ba.Material() + 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)) + self._real_wall_material=ba.Material() + self._real_wall_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + + self._real_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._goal_post_material=ba.Material() + self._goal_post_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + + self._goal_post_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + + )) + # 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', ba.DieMessage()))) + self._score_region_material = ba.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._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[ba.NodeActor]] = None + self._puck: Optional[Puck] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Score a goal.' + return 'Score ${ARG1} goals.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'score a goal' + return 'score ${ARG1} goals', self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._puck_spawn_pos =(0,16.9,-5.5) + self._spawn_puck() + self.make_map() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': (17,14.5,-5.52), + 'scale': (1,3,1), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': (-17,14.5,-5.52), + 'scale': (1,3,1), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._update_scoreboard() + ba.playsound(self._chant_sound) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = ba.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + puck.last_players_to_touch[player.team.id] = player + + def make_map(self): + shared = SharedObjects.get() + _ba.get_foreground_host_activity()._map.leftwall.materials= [shared.footing_material,self._real_wall_material ] + + _ba.get_foreground_host_activity()._map.rightwall.materials=[shared.footing_material,self._real_wall_material ] + + _ba.get_foreground_host_activity()._map.topwall.materials=[shared.footing_material,self._real_wall_material ] + floor="" + for i in range(0,90): + floor+="_ " + # self.floorwall=ba.newnode('region',attrs={'position': (-18.65152479, 4.057427485, -5.52),'scale': (72,2,6),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + self.floorwall=ba.newnode('region',attrs={'position': (0, 5, -5.52),'scale': (35.4,0.2,2),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + ba.newnode('locator', attrs={'shape':'box', 'position':(0, 5, -5.52), 'color':(0,0,0), 'opacity':1,'draw_beauty':True,'additive':False,'size':(35.4,0.2,2)}) + + # self.floor_text = ba.newnode('text', + # attrs={ + # 'text': floor, + # 'in_world': True, + # 'shadow': 1.0, + # 'flatness': 1.0, + # 'scale':0.019, + # 'h_align': 'center', + # 'position':(0,5.2,-5) + # }) + self.create_goal_post(-16.65,12.69) + self.create_goal_post(-16.65,16.69) + + self.create_goal_post(16.65,12.69) + self.create_goal_post(16.65,16.69) + + self.create_static_step(0,16.29) + + self.create_static_step(4.35,11.1) + self.create_static_step(-4.35,11.1) + + self.create_vertical(10, 15.6) + self.create_vertical(-10, 15.6) + + def create_static_step(self,x,y): + floor="" + for i in range(0,7): + floor+="_ " + shared = SharedObjects.get() + step={} + step["r"]=ba.newnode('region',attrs={'position': (x, y, -5.52),'scale': (3,0.1,6),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + ba.newnode('locator', attrs={'shape':'box', 'position':( x, y, -5.52), 'color':(1,1,0), 'opacity':1,'draw_beauty':True,'additive':False,'size':(3,0.1,2)}) + + # step["t"]=ba.newnode('text', + # attrs={ + # 'text': floor, + # 'in_world': True, + # 'shadow': 1.0, + # 'flatness': 1.0, + # 'scale':0.019, + # 'h_align': 'left', + # 'position':(x-1.2,y,-5.52) + # }) + return step + def create_goal_post(self,x,y): + shared = SharedObjects.get() + if x > 0: + color = (1,0,0) #change to team specific color + else: + color = (0,0,1) + floor="" + for i in range(0,4): + floor+="_ " + ba.newnode('region',attrs={'position': (x-0.2, y, -5.52),'scale': (1.8,0.1,6),'type': 'box','materials': [shared.footing_material,self._goal_post_material]}) + + ba.newnode('locator', attrs={'shape':'box', 'position':( x-0.2, y, -5.52), 'color': color, 'opacity':1,'draw_beauty':True,'additive':False,'size':(1.8,0.1,2)}) + # ba.newnode('text', + # attrs={ + # 'text': floor, + # 'in_world': True, + # 'color': color, + # 'shadow': 1.0, + # 'flatness': 1.0, + # 'scale':0.019, + # 'h_align': 'left', + # 'position':(x-1.2,y,-5.52) + # }) + + def create_vertical(self,x,y): + shared = SharedObjects.get() + floor = "" + for i in range(0,4): + floor +="|\n" + ba.newnode('region',attrs={'position': (x, y, -5.52),'scale': (0.1,2.8,1),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + ba.newnode('locator', attrs={'shape':'box', 'position':( x, y, -5.52), 'color':(1,1,0), 'opacity':1,'draw_beauty':True,'additive':False,'size':(0.1,2.8,2)}) + + # ba.newnode('text', + # attrs={ + # 'text': floor, + # 'in_world': True, + # 'shadow': 1.0, + # 'flatness': 1.0, + # 'scale':0.019, + # 'h_align': 'left', + # 'position':(x,y+1,-5.52) + # }) + + def spawn_player_spaz(self, + player: Player, + position: Sequence[float] = None, + angle: float = None) -> PlayerSpaz: + """Intercept new spazzes and add our team material for them.""" + if player.team.id==0: + position=(-10.75152479, 5.057427485, -5.52) + elif player.team.id==1: + position=(8.75152479, 5.057427485, -5.52) + + + spaz = super().spawn_player_spaz(player, position, angle) + return spaz + + def _kill_puck(self) -> None: + self._puck = None + + 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 = ba.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(ba.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() + + ba.playsound(self._foghorn_sound) + ba.playsound(self._cheer_sound) + + 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). + ba.timer(1.0, self._kill_puck) + + light = ba.newnode('light', + attrs={ + 'position': ba.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + ba.timer(1.0, light.delete) + + ba.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = ba.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, ba.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(): + ba.timer(3.0, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + light = ba.newnode('light', + attrs={ + 'position': self._puck_spawn_pos, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _spawn_puck(self) -> None: + ba.playsound(self._swipsound) + ba.playsound(self._whistle_sound) + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) diff --git a/dist/ba_root/mods/games/hot_potato.py b/dist/ba_root/mods/games/hot_potato.py new file mode 100644 index 0000000..6b4a2f8 --- /dev/null +++ b/dist/ba_root/mods/games/hot_potato.py @@ -0,0 +1,1039 @@ +""" + + Hot Potato by TheMikirog#1984 + + A random player(s) gets Marked. + They will die if they don't pass the mark to other players. + After they die, another random player gets Marked. + Last player standing wins! + + Heavily commented for easy modding learning! + + No Rights Reserved + +""" + +# ba_meta require api 7 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +# Define only what we need and nothing more +import ba +from bastd.actor.spaz import SpazFactory +from bastd.actor.spaz import PickupMessage +from bastd.actor.spaz import BombDiedMessage +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.bomb import Bomb +from bastd.actor.bomb import Blast +from enum import Enum +import random + +if TYPE_CHECKING: + pass + +# Let's define stun times for falling. +# First element is stun for the first fall, second element is stun for the second fall and so on. +# If we fall more than the amount of elements on this list, we'll use the last entry. +FALL_PENALTIES = [1.5, + 2.5, + 3.5, + 5.0, + 6.0, + 7.0, + 8.0, + 9.0, + 10.0] + +RED_COLOR = (1.0, 0.2, 0.2) +YELLOW_COLOR = (1.0, 1.0, 0.2) + + +# The player in Hot Potato can be in one of these states: +class PlayerState(Enum): + # REGULAR - the state all players start in. + REGULAR = 0 + # MARKED - when a player is marked, they'll be eliminated when the timer hits zero. + # Marked players pass the mark to REGULAR or STUNNED players by harming or grabbing other players. + # MARKED players respawn instantly if they somehow get knocked off the map. + MARKED = 1 + # ELIMINATED - a player is eliminated if the timer runs out during the MARKED state or they leave the game. + # These players can't win and won't respawn. + ELIMINATED = 2 + # STUNNED - if a REGULAR player falls out of the map, they'll receive the STUNNED state. + # STUNNED players are incapable of all movement and actions. + # STUNNED players can still get MARKED, but can't be punched, grabbed or knocked around by REGULAR players. + # STUNNED players will go back to the REGULAR state after several seconds. + # The time it takes to go back to the REGULAR state gets more severe the more times the player dies by falling off the map. + STUNNED = 3 + +# To make the game easier to parse, I added Elimination style icons to the bottom of the screen. +# Here's the behavior of each icon. +class Icon(ba.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: tuple[float, float], + scale: float, + name_scale: float = 1.0, + name_maxwidth: float = 100.0, + shadow: float = 1.0): + super().__init__() + + # Define the player this icon belongs to + self._player = player + self._name_scale = name_scale + + self._outline_tex = ba.gettexture('characterIconMask') + + # Character portrait + icon = player.get_icon() + self.node = ba.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' + }) + # Player name + self._name_text = ba.newnode( + 'text', + owner=self.node, + attrs={ + 'text': ba.Lstr(value=player.getname()), + 'color': ba.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + # Status text (such as Marked!, Stunned! and You're Out!) + self._marked_text = ba.newnode( + 'text', + owner=self.node, + attrs={ + 'text': '', + 'color': (1, 0.1, 0.0), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + # Status icon overlaying the character portrait + self._marked_icon = ba.newnode( + 'text', + owner=self.node, + attrs={ + 'text': ba.charstr(ba.SpecialChar.HAL), + 'color': (1, 1, 1), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 430, + 'shadow': 0.0, + 'opacity': 0.0, + 'flatness': 1.0, + 'scale': 2.1, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_marked_icon(player.state) + self.set_position_and_scale(position, scale) + + # Change our icon's appearance depending on the player state. + def set_marked_icon(self, type: PlayerState) -> None: + pos = self.node.position + # Regular players get no icons or status text + if type is PlayerState.REGULAR: + self._marked_icon.text = '' + self._marked_text.text = '' + self._marked_icon.opacity = 0.0 + self._name_text.flatness = 1.0 + assert self.node + self.node.color = (1.0, 1.0, 1.0) + # Marked players get ALL of the attention - red portrait, red text and icon overlaying the portrait + elif type is PlayerState.MARKED: + self._marked_icon.text = ba.charstr(ba.SpecialChar.HAL) + self._marked_icon.position = (pos[0] - 1, pos[1] - 13) + self._marked_icon.opacity = 1.0 + self._marked_text.text = 'Marked!' + self._marked_text.color = (1.0, 0.0, 0.0) + self._name_text.flatness = 0.0 + assert self.node + self.node.color = (1.0, 0.2, 0.2) + # Stunned players are just as important - yellow portrait, yellow text and moon icon. + elif type is PlayerState.STUNNED: + self._marked_icon.text = ba.charstr(ba.SpecialChar.MOON) + self._marked_icon.position = (pos[0] - 2, pos[1] - 12) + self._marked_icon.opacity = 1.0 + self._marked_text.text = 'Stunned!' + self._marked_text.color = (1.0, 1.0, 0.0) + assert self.node + self.node.color = (0.75, 0.75, 0.0) + # Eliminated players get special treatment. + # We make the portrait semi-transparent, while adding some visual flair with an fading skull icon and text. + elif type is PlayerState.ELIMINATED: + self._marked_icon.text = ba.charstr(ba.SpecialChar.SKULL) + self._marked_icon.position = (pos[0] - 2, pos[1] - 12) + self._marked_text.text = 'You\'re Out!' + self._marked_text.color = (0.5, 0.5, 0.5) + + # Animate text and icon + animation_end_time = 1.5 if bool(self.activity.settings['Epic Mode']) else 3.0 + ba.animate(self._marked_icon,'opacity', { + 0: 1.0, + animation_end_time: 0.0}) + ba.animate(self._marked_text,'opacity', { + 0: 1.0, + animation_end_time: 0.0}) + + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + else: + # If we beef something up, let the game know we made a mess in the code by providing a non-existant state. + raise Exception("invalid PlayerState type") + + # Set where our icon is positioned on the screen and how big it is. + 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 + self._marked_text.position = (position[0], position[1] - scale * 52.0) + self._marked_text.scale = 0.8 * scale + +# This gamemode heavily relies on edited player behavior. +# We need that amount of control, so we're gonna create our own class and use the original PlayerSpaz as our blueprint. +class PotatoPlayerSpaz(PlayerSpaz): + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) # unchanged Spaz __init__ code goes here + self.dropped_bombs = [] # we use this to track bombs thrown by the player + + # Define a marked light + self.marked_light = ba.newnode('light', + owner=self.node, + attrs={'position':self.node.position, + 'radius':0.15, + 'intensity':0.0, + 'height_attenuated':False, + 'color': (1.0, 0.0, 0.0)}) + + # Pulsing red light when the player is Marked + ba.animate(self.marked_light,'radius',{ + 0: 0.1, + 0.3: 0.15, + 0.6: 0.1}, + loop = True) + self.node.connectattr('position_center',self.marked_light,'position') + + # Marked timer. It should be above our head, so we attach the text to the offset that's attached to the player. + self.marked_timer_offset = ba.newnode('math', owner = self.node, attrs = { + 'input1': (0, 1.2, 0), + 'operation': 'add'}) + self.node.connectattr('torso_position', self.marked_timer_offset, 'input2') + + self.marked_timer_text = ba.newnode('text', owner = self.node, attrs = { + 'text': '', + 'in_world': True, + 'shadow': 0.4, + 'color': (RED_COLOR[0], RED_COLOR[1], RED_COLOR[2], 0.0), + 'flatness': 0, + 'scale': 0.02, + 'h_align': 'center'}) + self.marked_timer_offset.connectattr('output', self.marked_timer_text, 'position') + + # Modified behavior when dropping bombs + def drop_bomb(self) -> stdbomb.Bomb | None: + # The original function returns the Bomb the player created. + # This is super helpful for us, since all we need is to mark the bombs red + # if they belong to the Marked player and nothing else. + bomb = super().drop_bomb() + # Let's make sure the player actually created a new bomb + if bomb: + # Add our bomb to the list of our tracked bombs + self.dropped_bombs.append(bomb) + # Bring a light + bomb.bomb_marked_light = ba.newnode('light', + owner=bomb.node, + attrs={'position':bomb.node.position, + 'radius':0.04, + 'intensity':0.0, + 'height_attenuated':False, + 'color': (1.0, 0.0, 0.0)}) + # Attach the light to the bomb + bomb.node.connectattr('position',bomb.bomb_marked_light,'position') + # Let's adjust all lights for all bombs that we own. + self.set_bombs_marked() + # When the bomb physics node dies, call a function. + bomb.node.add_death_action( + ba.WeakCall(self.bomb_died, bomb)) + + + # Here's the function that gets called when one of the player's bombs dies. + # We reference the player's dropped_bombs list and remove the bomb that died. + def bomb_died(self, bomb): + self.dropped_bombs.remove(bomb) + + # Go through all the bombs this player has in the world. + # Paint them red if the owner is marked, turn off the light otherwise. + # We need this light to inform the player about bombs YOU DON'T want to get hit by. + def set_bombs_marked(self): + for bomb in self.dropped_bombs: + bomb.bomb_marked_light.intensity = 20.0 if self._player.state == PlayerState.MARKED else 0.0 + + # Since our gamemode relies heavily on players passing the mark to other players + # we need to have access to this message. This gets called when the player takes damage for any reason. + def handlemessage(self, msg): + if isinstance(msg, ba.HitMessage): + # This is basically the same HitMessage code as in the original Spaz. + # The only difference is that there is no health bar and you can't die with punches or bombs. + # Also some useless or redundant code was removed. + # I'm still gonna comment all of it since we're here. + if not self.node: + return None + + # If the attacker is marked, pass that mark to us. + self.activity.pass_mark(msg._source_player, self._player) + + # When stun timer runs out, we explode. Let's make sure our own explosion does throw us around. + if msg.hit_type == 'stun_blast' and msg._source_player == self.source_player: return True + # If the attacker is healthy and we're stunned, do a flash and play a sound, then ignore the rest of the code. + if self.source_player.state == PlayerState.STUNNED and msg._source_player != PlayerState.MARKED: + self.node.handlemessage('flash') + ba.playsound(SpazFactory.get().block_sound, + 1.0, + position=self.node.position) + return True + + # Here's all the damage and force calculations unchanged from the source. + mag = msg.magnitude * self.impact_scale + velocity_mag = msg.velocity_magnitude * self.impact_scale + damage_scale = 0.22 + + # We use them to apply a physical force to the player. + # Normally this is also used for damage, but we we're not gonna do it. + # We're still gonna calculate it, because it's still responsible for knockback. + 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], mag, + velocity_mag, msg.radius, 0, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + damage = int(damage_scale * self.node.damage) + self.node.handlemessage('hurt_sound') # That's how we play spaz node's hurt sound + + # Play punch impact sounds based on damage if it was a punch. + # We don't show damage percentages, because it's irrelevant. + if msg.hit_type == 'punch': + self.on_punched(damage) + + if damage >= 500: + sounds = SpazFactory.get().punch_sound_strong + sound = sounds[random.randrange(len(sounds))] + elif damage >= 100: + sound = SpazFactory.get().punch_sound + else: + sound = SpazFactory.get().punch_sound_weak + ba.playsound(sound, 1.0, position=self.node.position) + + # Throw up some chunks. + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 0.5, + msg.force_direction[1] * 0.5, + msg.force_direction[2] * 0.5), + count=min(10, 1 + int(damage * 0.0025)), + scale=0.3, + spread=0.03) + + ba.emitfx(position=msg.pos, + chunk_type='sweat', + velocity=(msg.force_direction[0] * 1.3, + msg.force_direction[1] * 1.3 + 5.0, + msg.force_direction[2] * 1.3), + count=min(30, 1 + int(damage * 0.04)), + scale=0.9, + spread=0.28) + + # Momentary flash. This spawns around where the Spaz's punch would be (we're kind of guessing here). + hurtiness = damage * 0.003 + punchpos = (msg.pos[0] + msg.force_direction[0] * 0.02, + msg.pos[1] + msg.force_direction[1] * 0.02, + msg.pos[2] + msg.force_direction[2] * 0.02) + flash_color = (1.0, 0.8, 0.4) + light = ba.newnode( + 'light', + attrs={ + 'position': punchpos, + 'radius': 0.12 + hurtiness * 0.12, + 'intensity': 0.3 * (1.0 + 1.0 * hurtiness), + 'height_attenuated': False, + 'color': flash_color + }) + ba.timer(0.06, light.delete) + + flash = ba.newnode('flash', + attrs={ + 'position': punchpos, + 'size': 0.17 + 0.17 * hurtiness, + 'color': flash_color + }) + ba.timer(0.06, flash.delete) + + # Physics collision particles. + if msg.hit_type == 'impact': + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 2.0, + msg.force_direction[1] * 2.0, + msg.force_direction[2] * 2.0), + count=min(10, 1 + int(damage * 0.01)), + scale=0.4, + spread=0.1) + + # Briefly flash when hit. + # We shouldn't do this if we're dead. + if self.hitpoints > 0: + + self.node.handlemessage('flash') + + # If we're holding something, drop it. + if damage > 0.0 and self.node.hold_node: + self.node.hold_node = None + # If we get grabbed, this function is called. + # We want to pass along the mark with grabs too. + elif isinstance(msg, PickupMessage): + # Make sure our body exists. + if not self.node: + return None + + # Let's get all collision data if we can. Otherwise cancel. + try: + collision = ba.getcollision() + opposingnode = collision.opposingnode + except ba.NotFoundError: + return True + + # Our grabber needs to be a Spaz + if opposingnode.getnodetype() == 'spaz': + # Disallow grabbing if a healthy player tries to grab us and we're stunned. + # If they're marked, continue with our scheduled program. + # It's the same sound and flashing behavior as hitting a stunned player as a healthy player. + if (opposingnode.source_player.state == PlayerState.STUNNED and self.source_player.state != PlayerState.MARKED): + opposingnode.handlemessage('flash') + ba.playsound(SpazFactory.get().block_sound, + 1.0, + position=opposingnode.position) + return True + # If they're marked and we're healthy or stunned, pass that mark along to us. + elif opposingnode.source_player.state in [PlayerState.REGULAR, PlayerState.STUNNED] and self.source_player.state == PlayerState.MARKED: + self.activity.pass_mark(self.source_player, opposingnode.source_player) + + # Our work is done. Continue with the rest of the grabbing behavior as usual. + super().handlemessage(msg) + # Dying is important in this gamemode and as such we need to address this behavior. + elif isinstance(msg, ba.DieMessage): + + # If a player left the game, inform our gamemode logic. + if msg.how == ba.DeathType.LEFT_GAME: + self.activity.player_left(self.source_player) + + # If a MARKED or STUNNED player dies, hide the text from the previous spaz. + if self.source_player.state in [PlayerState.MARKED, PlayerState.STUNNED]: + self.marked_timer_text.color = (self.marked_timer_text.color[0], + self.marked_timer_text.color[1], + self.marked_timer_text.color[2], + 0.0) + ba.animate(self.marked_light,'intensity',{ + 0: self.marked_light.intensity, + 0.5: 0.0}) + + # Continue with the rest of the behavior. + super().handlemessage(msg) + # If a message is something we haven't modified yet, let's pass it along to the original. + else: super().handlemessage(msg) + +# A concept of a player is very useful to reference if we don't have a player character present (maybe they died). +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + # Most of these are self explanatory. + self.icon: Icon = None + self.fall_times: int = 0 + self.state: PlayerState = PlayerState.REGULAR + self.stunned_time_remaining = None + # These are references to timers responsible for handling stunned behavior. + self.stunned_timer = None + self.stunned_update_timer = None + + # If we're stunned, a timer calls this every 0.1 seconds. + def stunned_timer_tick(self) -> None: + # Decrease our time remaining then change the text displayed above the Spaz's head + self.stunned_time_remaining -= 0.1 + self.stunned_time_remaining = max(0.0, self.stunned_time_remaining) + self.actor.marked_timer_text.text = str(round(self.stunned_time_remaining, 2)) + + # When stun time is up, call this function. + def stun_remove(self) -> None: + # Let's proceed only if we're stunned + if self.state != PlayerState.STUNNED: return + # Do an explosion where we're standing. Normally it would throw us around, but we dealt + # with this issue in PlayerSpaz's edited HitMessage in line 312. + Blast(position=self.actor.node.position, + velocity=self.actor.node.velocity, + blast_radius=2.5, + hit_type='stun_blast', # This hit type allows us to ignore our own stun blast explosions. + source_player=self).autoretain() + # Let's switch our state back to healthy. + self.set_state(PlayerState.REGULAR) + + # States are a key part of this gamemode and a lot of logic has to be done to acknowledge these state changes. + def set_state(self, state: PlayerState) -> None: + # Let's remember our old state before we change it. + old_state = self.state + + # If we just became stunned, do all of this: + if old_state != PlayerState.STUNNED and state == PlayerState.STUNNED: + self.actor.disconnect_controls_from_player() # Disallow all movement and actions + # Let's set our stun time based on the amount of times we fell out of the map. + if self.fall_times < len(FALL_PENALTIES): + stun_time = FALL_PENALTIES[self.fall_times] + else: + stun_time = FALL_PENALTIES[len(FALL_PENALTIES) - 1] + + self.stunned_time_remaining = stun_time # Set our stun time remaining + self.stunned_timer = ba.Timer(stun_time + 0.1, ba.Call(self.stun_remove)) # Remove our stun once the time is up + self.stunned_update_timer = ba.Timer(0.1, ba.Call(self.stunned_timer_tick), repeat = True) # Call a function every 0.1 seconds + self.fall_times += 1 # Increase the amount of times we fell by one + self.actor.marked_timer_text.text = str(stun_time) # Change the text above the Spaz's head to total stun time + + # If we were stunned, but now we're not, let's reconnect our controls. + # CODING CHALLENGE: to punch or bomb immediately after the stun ends, you need to + # time the button press frame-perfectly in order for it to work. + # What if we could press the button shortly before stun ends to do the action as soon as possible? + # If you're feeling up to the challenge, feel free to implement that! + if old_state == PlayerState.STUNNED and state != PlayerState.STUNNED: + self.actor.connect_controls_to_player() + + # When setting a state that is not STUNNED, clear all timers. + if state != PlayerState.STUNNED: + self.stunned_timer = None + self.stunned_update_timer = None + + # Here's all the light and text colors that we set depending on the state. + if state == PlayerState.MARKED: + self.actor.marked_light.intensity = 1.5 + self.actor.marked_light.color = (1.0, 0.0, 0.0) + self.actor.marked_timer_text.color = (RED_COLOR[0], + RED_COLOR[1], + RED_COLOR[2], + 1.0) + elif state == PlayerState.STUNNED: + self.actor.marked_light.intensity = 0.5 + self.actor.marked_light.color = (1.0, 1.0, 0.0) + self.actor.marked_timer_text.color = (YELLOW_COLOR[0], + YELLOW_COLOR[1], + YELLOW_COLOR[2], + 1.0) + else: + self.actor.marked_light.intensity = 0.0 + self.actor.marked_timer_text.text = '' + + self.state = state + self.actor.set_bombs_marked() # Light our bombs red if we're Marked, removes the light otherwise + self.icon.set_marked_icon(state) # Update our icon + + +# ba_meta export game +class HotPotato(ba.TeamGameActivity[Player, ba.Team]): + + # Let's define the basics like the name of the game, description and some tips that should appear at the start of a match. + name = 'Hot Potato' + description = ('A random player gets marked.\n' + 'Pass the mark to other players.\n' + 'Marked player gets eliminated when time runs out.\n' + 'Last one standing wins!') + tips = [ + 'You can pass the mark not only with punches and grabs, but bombs as well.', + 'If you\'re not marked, DON\'T fall off the map!\nEach fall will be punished with immobility.', + 'Falling can be a good escape strategy, but don\'t over rely on it.\nYou\'ll be defenseless if you respawn!', + 'Stunned players are immune to healthy players, but not to Marked players!', + 'Each fall when not Marked increases your time spent stunned.', + 'Try throwing healthy players off the map to make their timers\nlonger the next time they get stunned.', + 'Marked players don\'t get stunned when falling off the map.', + 'For total disrespect, try throwing the Marked player off the map\nwithout getting marked yourself!', + 'Feeling evil? Throw healthy players towards the Marked player!', + 'Red bombs belong to the Marked player!\nWatch out for those!', + 'Stunned players explode when their stun timer runs out.\nIf that time is close to zero, keep your distance!' + ] + + # We're gonna distribute end of match session scores based on who dies first and who survives. + # First place gets most points, then second, then third. + scoreconfig = ba.ScoreConfig(label='Place', + scoretype=ba.ScoreType.POINTS, + lower_is_better=True) + + # These variables are self explanatory too. + show_kill_points = False + allow_mid_activity_joins = False + + # Let's define some settings the user can mess around with to fit their needs. + available_settings = [ + ba.IntSetting('Elimination Timer', + min_value=5, + default=15, + increment=1, + ), + ba.BoolSetting('Marked Players use Impact Bombs', default=False), + ba.BoolSetting('Epic Mode', default=False), + ] + + # Hot Potato is strictly a Free-For-All gamemode, so only picking the gamemode in FFA playlists. + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.FreeForAllSession) + + # Most maps should work in Hot Potato. Generally maps marked as 'melee' are the most versatile map types of them all. + # As the name implies, fisticuffs are common forms of engagement. + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ba.getmaps('melee') + + # Here we define everything the gamemode needs, like sounds and settings. + def __init__(self, settings: dict): + super().__init__(settings) + self.settings = settings + + # Let's define all of the sounds we need. + self._tick_sound = ba.getsound('tick') + self._player_eliminated_sound = ba.getsound('playerDeath') + # These next sounds are arrays instead of single sounds. + # We'll use that fact later. + self._danger_tick_sounds = [ba.getsound('orchestraHit'), + ba.getsound('orchestraHit2'), + ba.getsound('orchestraHit3')] + self._marked_sounds = [ba.getsound('powerdown01'), + ba.getsound('activateBeep'), + ba.getsound('hiss')] + + # Normally play KOTH music, but switch to Epic music if we're in slow motion. + self._epic_mode = bool(settings['Epic Mode']) + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.SCARY) + + # This description appears below the title card after it comes crashing when the game begins. + def get_instance_description(self) -> str | Sequence: + return 'Pass the mark to someone else before you explode!' + + # This is the tiny text that is displayed in the corner during the game as a quick reminder of the objective. + def get_instance_description_short(self) -> str | Sequence: + return 'pass the mark' + + # Set up our player every time they join. + # Because you can't join mid-match, this will always be called at the beginning of the game. + def on_player_join(self, player: Player) -> None: + + player.state = PlayerState.REGULAR + player.fall_times = 0 + + # Create our icon and spawn. + if not self.has_begun(): + player.icon = Icon(player, position=(0, 50), scale=0.8) + self.spawn_player(player) + + # Returns every single marked player. + # This piece of info is used excensively in this gamemode, so it's advantageous to have a function to cut on + # work and make the gamemode easier to maintain + def get_marked_players(self) -> Sequence[ba.Player]: + marked_players = [] + for p in self.players: + if p.state == PlayerState.MARKED: + marked_players.append(p) + return marked_players + + # Marks a player. This sets their state, spawns some particles and sets the timer text above their heads. + def mark(self, target: Player) -> None: + target.set_state(PlayerState.MARKED) + + ba.emitfx(position=target.actor.node.position, + velocity=target.actor.node.velocity, + chunk_type='spark', + count=int(20.0+random.random()*20), + scale=1.0, + spread=1.0); + if bool(self.settings['Marked Players use Impact Bombs']): + target.actor.bomb_type = 'impact' + target.actor.marked_timer_text.text = str(self.elimination_timer_display) + + # Removes the mark from the player. This restores the player to its initial state. + def remove_mark(self, target: Player) -> None: + if target.state != PlayerState.MARKED: + return + + target.actor.bomb_type = 'normal' + + target.set_state(PlayerState.REGULAR) + target.actor.marked_timer_text.text = '' + + # Pass the mark from one player to another. + # This is more desirable than calling mark and remove_mark functions constantly and gives us + # more control over the mark spreading mechanic. + def pass_mark(self, marked_player: Player, hit_player: Player) -> None: + # Make sure both players meet the requirements + if not marked_player or not hit_player: return + if marked_player.state == PlayerState.MARKED and hit_player.state != PlayerState.MARKED: + self.mark(hit_player) + self.remove_mark(marked_player) + + # This function is called every second a marked player exists. + def _eliminate_tick(self) -> None: + marked_players = self.get_marked_players() + marked_player_amount = len(marked_players) + + # If there is no marked players, raise an exception. + # This is used for debugging purposes, which lets us know we messed up somewhere else in the code. + if len(self.get_marked_players()) == 0: + raise Exception("no marked players!") + + if self.elimination_timer_display > 1: + self.elimination_timer_display -= 1 # Decrease our timer by one second. + sound_volume = 1.0 / marked_player_amount + + for target in marked_players: + ba.playsound(self._tick_sound, sound_volume, target.actor.node.position) + target.actor.marked_timer_text.text = str(self.elimination_timer_display) + + # When counting down 3, 2, 1 play some dramatic sounds + if self.elimination_timer_display <= 3: + # We store our dramatic sounds in an array, so we target a specific element on the array + # depending on time remaining. Arrays start at index 0, so we need to decrease + # our variable by 1 to get the element index. + ba.playsound(self._danger_tick_sounds[self.elimination_timer_display - 1], 1.5) + else: + # Elimination timer is up! Let's eliminate all marked players. + self.elimination_timer_display -= 1 # Decrease our timer by one second. + self._eliminate_marked_players() + + # This function explodes all marked players + def _eliminate_marked_players(self) -> None: + self.marked_tick_timer = None + for target in self.get_marked_players(): + target.set_state(PlayerState.ELIMINATED) + target.actor.marked_timer_text.text = '' + + Blast(position=target.actor.node.position, + velocity=target.actor.node.velocity, + blast_radius=3.0, + source_player=target).autoretain() + ba.emitfx(position=target.actor.node.position, + velocity=target.actor.node.velocity, + count=int(16.0+random.random()*60), + scale=1.5, + spread=2, + chunk_type='spark') + target.actor.handlemessage(ba.DieMessage(how='marked_elimination')) + target.actor.shatter(extreme=True) + + self.match_placement.append(target.team) + + ba.playsound(self._player_eliminated_sound, 1.0) + + # Let the gamemode know a Marked + self.marked_players_died() + + # This function should be called when a Marked player dies, like when timer runs out or they leave the game. + def marked_players_died(self) -> bool: + alive_players = self.get_alive_players() + # Is there only one player remaining? Or none at all? Let's end the gamemode + if len(alive_players) < 2: + if len(alive_players) == 1: + self.match_placement.append(alive_players[0].team) # Let's add our lone survivor to the match placement list. + # Wait a while to let this sink in before we announce our victor. + self._end_game_timer = ba.Timer(1.25, ba.Call(self.end_game)) + else: + # There's still players remaining, so let's wait a while before marking a new player. + self.new_mark_timer = ba.Timer(2.0 if self.slow_motion else 4.0, ba.Call(self.new_mark)) + + # Another extensively used function that returns all alive players. + def get_alive_players(self) -> Sequence[ba.Player]: + alive_players = [] + for player in self.players: + if player.state == PlayerState.ELIMINATED: continue # Ignore players who have been eliminated + if player.is_alive(): + alive_players.append(player) + return alive_players + + # This function is called every time we want to start a new "round" by marking a random player. + def new_mark(self) -> None: + + # Don't mark a new player if we've already announced a victor. + if self.has_ended(): + return + + possible_targets = self.get_alive_players() + all_victims = [] + # Let's mark TWO players at once if there's 6 or more players. Helps with the pacing. + multi_choice = len(possible_targets) > 5 + + if multi_choice: + # Pick our first victim at random. + first_victim = random.choice(possible_targets) + all_victims.append(first_victim) + possible_targets.remove(first_victim) + # Let's pick our second victim, but this time excluding the player we picked earlier. + all_victims.append(random.choice(possible_targets)) + else: + # Pick one victim at random. + all_victims = [random.choice(possible_targets)] + + self.elimination_timer_display = self.settings['Elimination Timer'] # Set time until marked players explode + self.marked_tick_timer = ba.Timer(1.0, ba.Call(self._eliminate_tick), repeat=True) # Set a timer that calls _eliminate_tick every second + # Mark all chosen victims and play a sound + for new_victim in all_victims: + # _marked_sounds is an array. + # To make a nice marked sound effect, I play multiple sounds at once + # All of them are contained in the array. + for sound in self._marked_sounds: + ba.playsound(sound, 1.0, new_victim.actor.node.position) + self.mark(new_victim) + + # This function is called when the gamemode first loads. + def on_begin(self) -> None: + super().on_begin() # Do standard gamemode on_begin behavior + + self.elimination_timer_display = 0 + self.match_placement = [] + + # End the game if there's only one player + if len(self.players) < 2: + self.match_placement.append(self.players[0].team) + self._round_end_timer = ba.Timer(0.5, self.end_game) + else: + # Pick random player(s) to get marked + self.new_mark_timer = ba.Timer(2.0 if self.slow_motion else 5.2, ba.Call(self.new_mark)) + + self._update_icons() # Create player state icons + + # This function creates and positions player state icons + def _update_icons(self): + count = len(self.teams) + x_offs = 100 + xval = x_offs * (count - 1) * -0.5 + # FUN FACT: In FFA games, every player belongs to a one-player team. + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + player.icon.set_position_and_scale((xval, 50), 0.8) + xval += x_offs + + # Hot Potato can be a bit much, so I opted to show gameplay tips at the start of the match. + # However because I put player state icons, the tips overlay the icons. + # I'm gonna modify this function to move the tip text above the icons. + def _show_tip(self) -> None: + + from ba._gameutils import animate, GameTip + from ba._generated.enums import SpecialChar + from ba._language import Lstr + + # If there's any tips left on the list, display one. + if self.tips: + tip = self.tips.pop(random.randrange(len(self.tips))) + tip_title = Lstr(value='${A}:', + subs=[('${A}', Lstr(resource='tipText'))]) + icon: ba.Texture | None = None + sound: ba.Sound | None = None + if isinstance(tip, GameTip): + icon = tip.icon + sound = tip.sound + tip = tip.text + assert isinstance(tip, str) + + # Do a few replacements. + tip_lstr = Lstr(translate=('tips', tip), + subs=[('${PICKUP}', + ba.charstr(SpecialChar.TOP_BUTTON))]) + base_position = (75, 50) + tip_scale = 0.8 + tip_title_scale = 1.2 + vrmode = ba.app.vr_mode + + t_offs = -350.0 + height_offs = 100.0 + tnode = ba.newnode('text', + attrs={ + 'text': tip_lstr, + 'scale': tip_scale, + 'maxwidth': 900, + 'position': (base_position[0] + t_offs, + base_position[1] + height_offs), + 'h_align': 'left', + 'vr_depth': 300, + 'shadow': 1.0 if vrmode else 0.5, + 'flatness': 1.0 if vrmode else 0.5, + 'v_align': 'center', + 'v_attach': 'bottom' + }) + t2pos = (base_position[0] + t_offs - (20 if icon is None else 82), + base_position[1] + 2 + height_offs) + t2node = ba.newnode('text', + owner=tnode, + attrs={ + 'text': tip_title, + 'scale': tip_title_scale, + 'position': t2pos, + 'h_align': 'right', + 'vr_depth': 300, + 'shadow': 1.0 if vrmode else 0.5, + 'flatness': 1.0 if vrmode else 0.5, + 'maxwidth': 140, + 'v_align': 'center', + 'v_attach': 'bottom' + }) + if icon is not None: + ipos = (base_position[0] + t_offs - 40, base_position[1] + 1 + height_offs) + img = ba.newnode('image', + attrs={ + 'texture': icon, + 'position': ipos, + 'scale': (50, 50), + 'opacity': 1.0, + 'vr_depth': 315, + 'color': (1, 1, 1), + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) + ba.timer(5.0, img.delete) + if sound is not None: + ba.playsound(sound) + + combine = ba.newnode('combine', + owner=tnode, + attrs={ + 'input0': 1.0, + 'input1': 0.8, + 'input2': 1.0, + 'size': 4 + }) + combine.connectattr('output', tnode, 'color') + combine.connectattr('output', t2node, 'color') + animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) + ba.timer(5.0, tnode.delete) + + # This function is called when a player leaves the game. + # This is only called when the player already joined with a character. + def player_left(self, player: Player) -> None: + # If the leaving player is marked, remove the mark + if player.state == PlayerState.MARKED: + self.remove_mark(player) + + # If the leaving player is stunned, remove all stun timers + elif player.state == PlayerState.STUNNED: + player.stunned_timer = None + player.stunned_update_timer = None + + if len(self.get_marked_players()) == len(self.get_alive_players()): + for i in self.get_marked_players(): + self.remove_mark(i) + + if len(self.get_marked_players()) == 0: + self.marked_tick_timer = None + self.marked_players_died() + + player.set_state(PlayerState.ELIMINATED) + + # This function is called every time a player spawns + def spawn_player(self, player: Player) -> ba.Actor: + position = self.map.get_ffa_start_position(self.players) + position = (position[0], + position[1] - 0.3, # Move the spawn a bit lower + position[2]) + + name = player.getname() + + light_color = ba.normalized_color(player.color) + display_color = ba.safecolor(player.color, target_intensity=0.75) + + # Here we actually crate the player character + spaz = PotatoPlayerSpaz(color=player.color, + highlight=player.highlight, + character=player.character, + player=player) + spaz.node.invincible = False # Immediately turn off invincibility + player.actor = spaz # Assign player character to the owner + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + + # Move to the stand position and add a flash of light + spaz.handlemessage(ba.StandMessage(position, random.uniform(0, 360))) + t = ba.time(ba.TimeType.BASE) + ba.playsound(self._spawn_sound, 1.0, position=spaz.node.position) + light = ba.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + ba.animate(light, 'intensity', {0: 0, + 0.25: 1, + 0.5: 0}) + ba.timer(0.5, light.delete) + + # Game reacts to various events + def handlemessage(self, msg: Any) -> Any: + # This is called if the player dies. + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) # + player = msg.getplayer(Player) + + # If a player gets eliminated, don't respawn + if msg.how == 'marked_elimination': return + + self.spawn_player(player) # Spawn a new player character + + # If a REGULAR player dies, they respawn STUNNED. + # If a STUNNED player dies, reapply all visual effects. + if player.state in [PlayerState.REGULAR, PlayerState.STUNNED]: + player.set_state(PlayerState.STUNNED) + + # If a MARKED player falls off the map, apply the MARKED effects on the new spaz that respawns. + if player.state == PlayerState.MARKED: + self.mark(player) + + # This is called when we want to end the game and announce a victor + def end_game(self) -> None: + # Proceed only if the game hasn't ended yet. + if self.has_ended(): + return + results = ba.GameResults() + # By this point our match placement list should be filled with all players. + # Players that died/left earliest should be the first entries. + # We're gonna use array indexes to decide match placements. + # Because of that, we're gonna flip the order of our array, so the last entries are first. + self.match_placement.reverse() + for team in self.teams: + # Use each player's index in the array for our scoring + # 0 is the first index, so we add 1 to the score. + results.set_team_score(team, self.match_placement.index(team) + 1) + self.end(results=results) # Standard game ending behavior diff --git a/dist/ba_root/mods/maps/BridgitPlus.py b/dist/ba_root/mods/maps/BridgitPlus.py new file mode 100644 index 0000000..261cfa7 --- /dev/null +++ b/dist/ba_root/mods/maps/BridgitPlus.py @@ -0,0 +1,230 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba,_ba +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, List, Dict +class mapdefs: + points = {} + # noinspection PyDictCreation + boxes = {} + boxes['area_of_interest_bounds'] = (-0.2457963347, 3.828181068, + -1.528362695) + (0.0, 0.0, 0.0) + ( + 19.14849937, 7.312788846, 13.436232726) + points['ffa_spawn1'] = (-5.869295124, 3.715437928, + -1.617274877) + (0.9410329222, 1.0, 1.818908238) + points['ffa_spawn2'] = (5.160809653, 3.761793434, + -1.443012115) + (0.7729807005, 1.0, 1.818908238) + points['ffa_spawn3'] = (-0.4266381164, 3.761793434, + -1.555562653) + (4.034151421, 1.0, 0.2731725824) + points['flag1'] = (-7.354603923, 3.770769731, -1.617274877) + points['flag2'] = (6.885846926, 3.770685211, -1.443012115) + points['flag_default'] = (-0.2227795102, 3.802429326, -1.562586233) + boxes['map_bounds'] = (-0.1916036665, 7.481446847, -1.311948055) + ( + 0.0, 0.0, 0.0) + (27.41996888, 18.47258973, 19.52220249) + points['powerup_spawn1'] = (6.82849491, 4.658454461, 0.1938139802) + points['powerup_spawn2'] = (-7.253381358, 4.728692078, 0.252121017) + points['powerup_spawn3'] = (6.82849491, 4.658454461, -3.461765427) + points['powerup_spawn4'] = (-7.253381358, 4.728692078, -3.40345839) + points['shadow_lower_bottom'] = (-0.2227795102, 2.83188898, 2.680075641) + points['shadow_lower_top'] = (-0.2227795102, 3.498267184, 2.680075641) + points['shadow_upper_bottom'] = (-0.2227795102, 6.305086402, 2.680075641) + points['shadow_upper_top'] = (-0.2227795102, 9.470923628, 2.680075641) + points['spawn1'] = (-5.869295124, 3.715437928, + -1.617274877) + (0.9410329222, 1.0, 1.818908238) + points['spawn2'] = (5.160809653, 3.761793434, + -1.443012115) + (0.7729807005, 1.0, 1.818908238) +class BridgitPlus(ba.Map): + """Map with a narrow bridge in the middle.""" + # from bastd.mapdata import bridgit as defs + defs = mapdefs + name = 'Bridgit Plus' + dataname = 'bridgit' + + @classmethod + def get_play_types(cls) -> list[str]: + """Return valid play types for this map.""" + # print('getting playtypes', cls._getdata()['play_types']) + return ['melee', 'team_flag', 'keep_away'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'bridgitPreview' + + @classmethod + def on_preload(cls) -> Any: + data: dict[str, Any] = { + 'model_top': ba.getmodel('bridgitLevelTop'), + 'model_bottom': ba.getmodel('bridgitLevelBottom'), + 'model_bg': ba.getmodel('natureBackground'), + 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), + 'collide_model': ba.getcollidemodel('bridgitLevelCollide'), + 'tex': ba.gettexture('bridgitLevelColor'), + 'model_bg_tex': ba.gettexture('natureBackgroundColor'), + 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), + 'railing_collide_model': + (ba.getcollidemodel('bridgitLevelRailingCollide')), + 'bg_material': ba.Material() + } + data['bg_material'].add_actions(actions=('modify_part_collision', + 'friction', 10.0)) + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model_top'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['model_bottom'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['model_bg'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bg_vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + + self.bg_collide = ba.newnode('terrain', + attrs={ + 'collide_model': + self.preloaddata['collide_bg'], + 'materials': [ + shared.footing_material, + self.preloaddata['bg_material'], + shared.death_material + ] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.2, 1.3) + gnode.ambient_color = (1.1, 1.2, 1.3) + gnode.vignette_outer = (0.65, 0.6, 0.55) + gnode.vignette_inner = (0.9, 0.9, 0.93) + self.map_extend() + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + box_position = self.defs.boxes['edge_box'][0:3] + box_scale = self.defs.boxes['edge_box'][6:9] + xpos = (point.x - box_position[0]) / box_scale[0] + zpos = (point.z - box_position[2]) / box_scale[2] + return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 + + def map_extend(self): + + shared = SharedObjects.get() + self._real_wall_material=ba.Material() + + self._real_wall_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.mat = ba.Material() + self.mat.add_actions( + + actions=( ('modify_part_collision','physical',False), + ('modify_part_collision','collide',False)) + ) + spaz_collide_mat=ba.Material() + + pos=(-1.8323341846466064, 3.004164695739746, -1.3991328477859497) + self.ud_1_r=ba.newnode('region',attrs={'position': pos,'scale': (2,1,2),'type': 'box','materials': [shared.footing_material,self._real_wall_material,spaz_collide_mat ]}) + + self.node = ba.newnode('prop', + owner=self.ud_1_r, + attrs={ + 'model':ba.getmodel('bridgitLevelTop'), + 'light_model':ba.getmodel('powerupSimple'), + 'position':(2,7,2), + 'body':'puck', + 'shadow_size':0.0, + 'velocity':(0,0,0), + 'color_texture':ba.gettexture('bridgitLevelColor'), + + 'reflection_scale':[1.5], + 'materials':[self.mat, shared.object_material,shared.footing_material], + + 'density':9000000000 + }) + self.node.changerotation(0,1,0) + mnode = ba.newnode('math', + owner=self.ud_1_r, + attrs={ + 'input1': (0, -2.9, 0), + 'operation': 'add' + }) + + self.ud_1_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', self.node, 'position') + + + # base / bottom ==================================== + + pos=(-1.8323341846466064, 2.004164695739746, -1.3991328477859497) + self.ud_2_r=ba.newnode('region',attrs={'position': pos,'scale': (2,1,2),'type': 'box','materials': [shared.footing_material,self._real_wall_material,spaz_collide_mat ]}) + + self.node2 = ba.newnode('prop', + owner=self.ud_2_r, + attrs={ + 'model':ba.getmodel('bridgitLevelBottom'), + 'light_model':ba.getmodel('powerupSimple'), + 'position':(2,7,2), + 'body':'puck', + 'shadow_size':0.0, + 'velocity':(0,0,0), + 'color_texture':ba.gettexture('bridgitLevelColor'), + + 'reflection_scale':[1.5], + 'materials':[self.mat, shared.object_material,shared.footing_material], + + 'density':9000000000 + }) + mnode = ba.newnode('math', + owner=self.ud_2_r, + attrs={ + 'input1': (0, -1.8, 0), + 'operation': 'add' + }) + self.node2.changerotation(0,1,0) + self.ud_2_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', self.node2, 'position') + + # /// region to stand long bar =============== + + + pos=(-0.26, 3.204164695739746, -5.3991328477859497) + self.v_region=ba.newnode('region',attrs={'position': pos,'scale': (1.5,1,21),'type': 'box','materials': [shared.footing_material,self._real_wall_material,spaz_collide_mat ]}) + pos=(-0.26, 3.204164695739746, -7.5) + self.h_1_region=ba.newnode('region',attrs={'position': pos,'scale': (4.9,1,3.6),'type': 'box','materials': [shared.footing_material,self._real_wall_material,spaz_collide_mat ]}) + + pos=(-0.42, 3.204164695739746, 4.1) + self.h_1_region=ba.newnode('region',attrs={'position': pos,'scale': (4.9,1,3.6),'type': 'box','materials': [shared.footing_material,self._real_wall_material,spaz_collide_mat ]}) + +ba._map.register_map(BridgitPlus) \ No newline at end of file diff --git a/dist/ba_root/mods/maps/CreativeThoughts.so b/dist/ba_root/mods/maps/CreativeThoughts.so deleted file mode 100644 index f84b2d6..0000000 Binary files a/dist/ba_root/mods/maps/CreativeThoughts.so and /dev/null differ diff --git a/dist/ba_root/mods/maps/DesertMap.py b/dist/ba_root/mods/maps/DesertMap.py new file mode 100644 index 0000000..21827a5 --- /dev/null +++ b/dist/ba_root/mods/maps/DesertMap.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba,_ba +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, List, Dict + +# This file was automatically generated from "big_g.ma" +# pylint: disable=all +class mapdefs: + points = {} + # noinspection PyDictCreation + boxes = {} + boxes['area_of_interest_bounds'] = (-1.4011866709, -1.331310176, + -4.5426286416) + (0.0, 0.0, 0.0) + ( + 19.11746262, 10.19675564, 25.50119277) + points['ffa_spawn1'] = (3.140826121, -1.16512015, + 6.172121491) + (4.739204545, 1-1.0, 1.028864849) + points['ffa_spawn2'] = (5.416289073, -1.180022599, -0.1696495695) + ( + 2.945888237, -0.621599724, 0.4969830881) + points['ffa_spawn3'] = (-0.3692088357, -1.88984723, -6.909741615) + ( + 7.575371952, 0.621599724, 0.4969830881) + points['ffa_spawn4'] = (-2.391932409, -1.123690253, -3.417262271) + ( + 2.933065031, 0.621599724, 0.9796558695) + points['ffa_spawn5'] = (-7.46052038, -1.863807079, + 4.936420902) + (0.8707600789, -0.621599724, 2.233577195) + points['flag1'] = (7.557928387, 2.889342613, -7.208799596) + points['flag2'] = (7.696183956, 1.095466627, 6.103380446) + points['flag3'] = (-8.122819332, 2.844893069, 6.103380446) + points['flag4'] = (-8.018537918, 2.844893069, -6.202403896) + points['flag_default'] = (-7.563673017, 2.850652319, 0.08844978098) + boxes['map_bounds'] = (-7.1916036665, -3.764115729, -43.1971423239) + ( + 0.0, 0.0, 0.0) + (48.41996888, 100.47258973, 150.17335735) + points['powerup_spawn1'] = (7.830495287, -1.115087683, -0.05452287857) + points['powerup_spawn2'] = (-5.190293739, -1.476317443, -3.80237889) + points['powerup_spawn3'] = (-8.540957726, -1.762979519, -7.27710542) + points['powerup_spawn4'] = (7.374052727, -1.762979519, -3.091707631) + points['powerup_spawn5'] = (-8.691423338, -1.692026034, 6.627877455) + points['race_mine1'] = (-0.06161453294, 1.123140909, 4.966104324) + points['race_mine10'] = (-6.870248758, 2.851484105, 2.718992803) + points['race_mine2'] = (-0.06161453294, 1.123140909, 6.99632996) + points['race_mine3'] = (-0.7319278377, 1.123140909, -2.828583367) + points['race_mine4'] = (-3.286508423, 1.123140909, 0.8453899305) + points['race_mine5'] = (5.077545429, 2.850225463, -5.253575631) + points['race_mine6'] = (6.286453838, 2.850225463, -5.253575631) + points['race_mine7'] = (0.969120762, 2.851484105, -7.892038145) + points['race_mine8'] = (-2.976299166, 2.851484105, -6.241064664) + points['race_mine9'] = (-6.962812986, 2.851484105, -2.120262964) + points['race_point1'] = (2.280447713, 1.16512015, 6.015278429) + ( + 0.7066894139, 4.672784871, 1.322422256) + points['race_point10'] = (-4.196540687, 2.877461266, -7.106874334) + ( + 0.1057202515, 5.496127671, 1.028552836) + points['race_point11'] = (-7.634488499, 2.877461266, -3.61728743) + ( + 1.438144134, 5.157457566, 0.06318119808) + points['race_point12'] = (-7.541251512, 2.877461266, 3.290439202) + ( + 1.668578284, 5.52484043, 0.06318119808) + points['race_point2'] = (4.853459878, 1.16512015, + 6.035867283) + (0.3920628436, 4.577066678, 1.34568243) + points['race_point3'] = (6.905234402, 1.16512015, 1.143337503) + ( + 1.611663691, 3.515259775, 0.1135135003) + points['race_point4'] = (2.681673258, 1.16512015, 0.771967064) + ( + 0.6475414982, 3.602143342, 0.1135135003) + points['race_point5'] = (-0.3776550727, 1.225615225, 1.920343787) + ( + 0.1057202515, 4.245024435, 0.5914887576) + points['race_point6'] = (-4.365081958, 1.16512015, -0.3565529313) + ( + 1.627090525, 4.549428479, 0.1135135003) + points['race_point7'] = (0.4149308672, 1.16512015, -3.394316313) + ( + 0.1057202515, 4.945367833, 1.310190117) + points['race_point8'] = (4.27031635, 2.19747021, -3.335165617) + ( + 0.1057202515, 4.389664492, 1.20413595) + points['race_point9'] = (2.552998384, 2.877461266, -7.117366939) + ( + 0.1057202515, 5.512312989, 0.9986814472) + points['shadow_lower_bottom'] = (-0.2227795102, 0.2903873918, 2.680075641) + points['shadow_lower_top'] = (-0.2227795102, 0.8824975157, 2.680075641) + points['shadow_upper_bottom'] = (-0.2227795102, 6.305086402, 2.680075641) + points['shadow_upper_top'] = (-0.2227795102, 9.470923628, 2.680075641) + points['spawn1'] = (3.180043217, -3.85596295, -1.407134234) + (0.7629937742, + -1.0, 1.818908238) + points['spawn2'] = (-5.880548999, -3.142163379, -2.171168951) + (1.817516622, -1.0, + 0.7724344394) + points['spawn_by_flag1'] = (7.180043217, 2.85596295, + -4.407134234) + (0.7629937742, 1.0, 1.818908238) + points['spawn_by_flag2'] = (5.880548999, 1.142163379, + 6.171168951) + (1.817516622, 1.0, 0.7724344394) + points['spawn_by_flag3'] = (-6.66642559, 3.554416948, + 5.820238985) + (1.097315815, 1.0, 1.285161684) + points['spawn_by_flag4'] = (-6.842951255, 3.554416948, + -6.17429905) + (0.8208434737, 1.0, 1.285161684) + points['tnt1'] = (-3.398312776, 2.067056737, -1.90142919) + +class Desert(ba.Map): + """Large G shaped map for racing""" + + defs = mapdefs + + name = 'Desert' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [ + 'race', 'melee', 'keep_away', 'team_flag', 'king_of_the_hill', + 'conquest' + ] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'bigGPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model_top': ba.getmodel('bigG'), + 'model_bottom': ba.getmodel('bigGBottom'), + 'model_bg': ba.getmodel('natureBackground'), + 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), + 'collide_model': ba.getcollidemodel('bigGCollide'), + 'tex': ba.gettexture('bigG'), + 'model_bg_tex': ba.gettexture('natureBackgroundColor'), + 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), + 'bumper_collide_model': ba.getcollidemodel('bigGBumper'), + 'bg_material': ba.Material() + } + data['bg_material'].add_actions(actions=('modify_part_collision', + 'friction', 3.0)) + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['model_bg'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + + self.bg_collide = ba.newnode('terrain', + attrs={ + 'collide_model': + self.preloaddata['collide_bg'], + 'materials': [ + shared.footing_material, + self.preloaddata['bg_material'], + + ] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.2, 1.3) + gnode.ambient_color = (1.1, 1.2, 1.3) + gnode.vignette_outer = (0.65, 0.6, 0.55) + gnode.vignette_inner = (0.9, 0.9, 0.93) + + +ba._map.register_map(Desert) \ No newline at end of file diff --git a/dist/ba_root/mods/maps/LakeOfDeath.py b/dist/ba_root/mods/maps/LakeOfDeath.py new file mode 100644 index 0000000..7c2f3a8 --- /dev/null +++ b/dist/ba_root/mods/maps/LakeOfDeath.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba,_ba +from bastd.gameutils import SharedObjects +from bastd.actor.playerspaz import PlayerSpaz +if TYPE_CHECKING: + from typing import Any, List, Dict + + +class mapdefs: + points = {} + # noinspection PyDictCreation + boxes = {} + boxes['area_of_interest_bounds'] = (0.0, 1.185751251, 0.4326226188) + ( + 0.0, 0.0, 0.0) + (29.8180273, 11.57249038, 18.89134176) + boxes['edge_box'] = (-0.103873591, 0.4133341891, 0.4294651013) + ( + 0.0, 0.0, 0.0) + (22.48295719, 1.290242794, 8.990252454) + points['ffa_spawn1'] = (-0.08015551329, 0.02275111462, + -4.373674593) + (8.895057015, 1.0, 0.444350722) + points['ffa_spawn2'] = (-0.08015551329, 0.02275111462, + 4.076288941) + (8.895057015, 1.0, 0.444350722) + points['flag1'] = (-10.99027878, 0.05744967453, 0.1095578275) + points['flag2'] = (11.01486398, 0.03986567039, 0.1095578275) + points['flag_default'] = (-0.1001374046, 0.04180340146, 0.1095578275) + boxes['goal1'] = (12.22454533, 1.0, + 0.1087926362) + (0.0, 0.0, 0.0) + (2.0, 2.0, 12.97466313) + boxes['goal2'] = (-12.15961605, 1.0, + 0.1097860203) + (0.0, 0.0, 0.0) + (2.0, 2.0, 13.11856424) + boxes['map_bounds'] = (0.0, 1.185751251, 0.4326226188) + (0.0, 0.0, 0.0) + ( + 42.09506485, 22.81173179, 29.76723155) + points['powerup_spawn1'] = (5.414681236, 0.9515026107, -5.037912441) + points['powerup_spawn2'] = (-5.555402285, 0.9515026107, -5.037912441) + points['powerup_spawn3'] = (5.414681236, 0.9515026107, 5.148223181) + points['powerup_spawn4'] = (-5.737266365, 0.9515026107, 5.148223181) + points['spawn1'] = (-10.03866341, 0.02275111462, 0.0) + (0.5, 1.0, 4.0) + points['spawn2'] = (9.823107149, 0.01092306765, 0.0) + (0.5, 1.0, 4.0) + points['tnt1'] = (-0.08421587483, 0.9515026107, -0.7762602271) + +class LakeOfDeath(ba.Map): + """Stadium map for football games.""" + defs = mapdefs + defs.points['spawn1'] = (-12.03866341, 0.02275111462, 0.0) + (0.5, 1.0, 4.0) + defs.points['spawn2'] = (12.823107149, 0.01092306765, 0.0) + (0.5, 1.0, 4.0) + name = 'Lake of Death' + + @classmethod + def get_play_types(cls) -> list[str]: + """Return valid play types for this map.""" + return ['melee', 'football', 'team_flag', 'keep_away'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'footballStadiumPreview' + + @classmethod + def on_preload(cls) -> Any: + data: dict[str, Any] = { + 'model': ba.getmodel('footballStadium'), + 'vr_fill_model': ba.getmodel('footballStadiumVRFill'), + 'collide_model': ba.getcollidemodel('footballStadiumCollide'), + 'tex': ba.gettexture('footballStadium') + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'model': self.preloaddata['model'], + 'collide_model': self.preloaddata['collide_model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['tex'] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.3, 1.2, 1.0) + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + self.map_extend() + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + box_position = self.defs.boxes['edge_box'][0:3] + box_scale = self.defs.boxes['edge_box'][6:9] + xpos = (point.x - box_position[0]) / box_scale[0] + zpos = (point.z - box_position[2]) / box_scale[2] + return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 + + def map_extend(self): + self.create_ramp(0) + self.create_ramp(10.9) + self.ground() + + def ground(self): + shared = SharedObjects.get() + self._real_wall_material=ba.Material() + + self._real_wall_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.mat = ba.Material() + self.mat.add_actions( + + actions=( ('modify_part_collision','physical',False), + ('modify_part_collision','collide',False)) + ) + spaz_collide_mat=ba.Material() + spaz_collide_mat.add_actions( + conditions=('they_have_material',shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ( 'call','at_connect',ba.Call(self._handle_player_collide )), + ), + ) + pos=(0,0.1,-5) + self.main_region=ba.newnode('region',attrs={'position': pos,'scale': (21,0.001,23),'type': 'box','materials': [shared.footing_material,self._real_wall_material,spaz_collide_mat]}) + + + def create_ramp(self,loc): + z_marg=0 + if loc!=0: + z_marg=0.3 + + shared = SharedObjects.get() + self._real_wall_material=ba.Material() + + self._real_wall_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.mat = ba.Material() + self.mat.add_actions( + + actions=( ('modify_part_collision','physical',False), + ('modify_part_collision','collide',False)) + ) + spaz_collide_mat=ba.Material() + + pos=(-5.3 +loc,0.7,1.1+z_marg) + self.ud_1_r=ba.newnode('region',attrs={'position': pos,'scale': (2,1,2),'type': 'box','materials': [shared.footing_material,spaz_collide_mat ]}) + + self.node = ba.newnode('prop', + owner=self.ud_1_r, + attrs={ + 'model':ba.getmodel('bridgitLevelTop'), + 'light_model':ba.getmodel('powerupSimple'), + 'position':(2,7,2), + 'body':'puck', + 'shadow_size':0.0, + 'velocity':(0,0,0), + 'color_texture':ba.gettexture('bridgitLevelColor'), + 'model_scale':0.72, + 'reflection_scale':[1.5], + 'materials':[self.mat, shared.object_material,shared.footing_material], + + 'density':9000000000 + }) + self.node.changerotation(0,0,0) + mnode = ba.newnode('math', + owner=self.ud_1_r, + attrs={ + 'input1': (0, -2.9, 0), + 'operation': 'add' + }) + + + self.ud_1_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', self.node, 'position') + + + pos=(-9.67+loc,0.1,0+z_marg) + self.left_region=ba.newnode('region',attrs={'position': pos,'scale': (2.4,0.4,3.4),'type': 'box','materials': [shared.footing_material,self._real_wall_material,spaz_collide_mat ]}) + + pos=(-5.67+loc,0.1,0+z_marg) + self.center_region=ba.newnode('region',attrs={'position': pos,'scale': (8,0.4,1),'type': 'box','materials': [shared.footing_material,self._real_wall_material,spaz_collide_mat ]}) + pos=(-1.3+loc-0.1,0.1,0+z_marg) + self.right_region=ba.newnode('region',attrs={'position': pos,'scale': (2.6,0.4,3.7),'type': 'box','materials': [shared.footing_material,self._real_wall_material,spaz_collide_mat ]}) + + + def _handle_player_collide(self): + try: + player = ba.getcollision().opposingnode.getdelegate( + PlayerSpaz, True) + except ba.NotFoundError: + return + + + if player.is_alive(): + player.shatter(True) + + + + +ba._map.register_map(LakeOfDeath) \ No newline at end of file diff --git a/dist/ba_root/mods/maps/WoodenFloor.py b/dist/ba_root/mods/maps/WoodenFloor.py index 27229af..f4af6d9 100644 --- a/dist/ba_root/mods/maps/WoodenFloor.py +++ b/dist/ba_root/mods/maps/WoodenFloor.py @@ -108,6 +108,99 @@ class WoodenFloor(ba.Map): zpos = (point.z - box_position[2]) / box_scale[2] return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 + def map_extend(self): + pass + # p=[-6,-4.3,-2.6,-0.9,0.8,2.5,4.2,5.9] + # q=[-4,-2.3,-0.6,1.1,2.8,4.5,6.2] + # for i in p: + # for j in q: + # self.create_ramp(i,j) + + # self.create_ramp(10.9) + # self.ground() + + def ground(self): + shared = SharedObjects.get() + self._real_wall_material=ba.Material() + + self._real_wall_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.mat = ba.Material() + self.mat.add_actions( + + actions=( ('modify_part_collision','physical',False), + ('modify_part_collision','collide',False)) + ) + spaz_collide_mat=ba.Material() + spaz_collide_mat.add_actions( + conditions=('they_have_material',shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ( 'call','at_connect',ba.Call(self._handle_player_collide )), + ), + ) + pos=(0,0.1,-5) + self.main_region=ba.newnode('region',attrs={'position': pos,'scale': (21,0.001,20),'type': 'box','materials': [shared.footing_material,self._real_wall_material,spaz_collide_mat]}) + + + def create_ramp_111(self,x,z): + + shared = SharedObjects.get() + self._real_wall_material=ba.Material() + + self._real_wall_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.mat = ba.Material() + self.mat.add_actions( + + actions=( ('modify_part_collision','physical',False), + ('modify_part_collision','collide',False)) + ) + spaz_collide_mat=ba.Material() + + pos=(x,0,z) + ud_1_r=ba.newnode('region',attrs={'position': pos,'scale': (1.5,1,1.5),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + + node = ba.newnode('prop', + owner=ud_1_r, + attrs={ + 'model':ba.getmodel('image1x1'), + 'light_model':ba.getmodel('powerupSimple'), + 'position':(2,7,2), + 'body':'puck', + 'shadow_size':0.0, + 'velocity':(0,0,0), + 'color_texture':ba.gettexture('tnt'), + 'model_scale':1.5, + 'reflection_scale':[1.5], + 'materials':[self.mat, shared.object_material,shared.footing_material], + 'density':9000000000 + }) + mnode = ba.newnode('math', + owner=ud_1_r, + attrs={ + 'input1': (0, 0.6, 0), + 'operation': 'add' + }) + + + node.changerotation(1,0,0) + ud_1_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', node, 'position') + + + + def _handle_player_collide(self): try: player = ba.getcollision().opposingnode.getdelegate( diff --git a/dist/ba_root/mods/maps/creative_thoughts.py b/dist/ba_root/mods/maps/creative_thoughts.py new file mode 100644 index 0000000..b094eeb --- /dev/null +++ b/dist/ba_root/mods/maps/creative_thoughts.py @@ -0,0 +1,191 @@ + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, List, Dict + +class mapdefs: + points = {} + # noinspection PyDictCreation + boxes = {} + boxes['area_of_interest_bounds'] = (-1.045859963, 12.67722855, + -5.401537075) + (0.0, 0.0, 0.0) + ( + 42.46156851, 20.94044653, 0.6931564611) + points['ffa_spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) + points['ffa_spawn2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) + points['ffa_spawn3'] = (9.55724115, 11.30789446, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn4'] = (-11.55747023, 10.99170684, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn5'] = (-1.878892369, 9.46490571, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) + points['ffa_spawn6'] = (-0.4912812943, 5.077006397, -5.521672101) + ( + 1.878332089, 1.453808816, 0.007578097856) + points['flag1'] = (-11.75152479, 8.057427485, -5.52) + points['flag2'] = (9.840909039, 8.188634282, -5.52) + points['flag3'] = (-0.2195258696, 5.010273907, -5.52) + points['flag4'] = (-0.04605809154, 12.73369108, -5.52) + points['flag_default'] = (-0.04201942896, 12.72374492, -5.52) + boxes['map_bounds'] = (-0.8748348681, 9.212941713, -5.729538885) + ( + 0.0, 0.0, 0.0) + (42.09666006, 26.19950145, 7.89541168) + points['powerup_spawn1'] = (1.160232442, 6.745963662, -5.469115985) + points['powerup_spawn2'] = (-1.899700206, 10.56447241, -5.505721177) + points['powerup_spawn3'] = (10.56098871, 12.25165669, -5.576232453) + points['powerup_spawn4'] = (-12.33530337, 12.25165669, -5.576232453) + points['spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) + points['spawn2'] = (7.484707127, 8.172681752, + -5.614479365) + (1.553861796, 1.453808816, 0.04419853907) + points['spawn_by_flag1'] = (-9.295167711, 8.010664315, -5.44451005) + ( + 1.555840357, 1.453808816, 0.1165648888) + points['spawn_by_flag2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) + points['spawn_by_flag3'] = (-1.45994593, 5.038762459, -5.535288724) + ( + 0.9516389866, 0.6666414677, 0.08607244075) + points['spawn_by_flag4'] = (0.4932087091, 12.74493212, -5.598987003) + ( + 0.5245740665, 0.5245740665, 0.01941146064) +class CreativeThoughts(ba.Map): + """Freaking map by smoothy.""" + + defs = mapdefs + + name = 'Creative Thoughts' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [ + 'melee', 'keep_away', 'team_flag' + ] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'alwaysLandPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('alwaysLandLevel'), + 'bottom_model': ba.getmodel('alwaysLandLevelBottom'), + 'bgmodel': ba.getmodel('alwaysLandBG'), + 'collide_model': ba.getcollidemodel('alwaysLandLevelCollide'), + 'tex': ba.gettexture('alwaysLandLevelColor'), + 'bgtex': ba.gettexture('alwaysLandBGColor'), + 'vr_fill_mound_model': ba.getmodel('alwaysLandVRFillMound'), + 'vr_fill_mound_tex': ba.gettexture('vrFillMound') + } + return data + + @classmethod + def get_music_type(cls) -> ba.MusicType: + return ba.MusicType.FLYING + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, -3.7, 2.5)) + shared = SharedObjects.get() + self._fake_wall_material=ba.Material() + self._real_wall_material=ba.Material() + self._fake_wall_material.add_actions( + conditions=(('they_are_younger_than',9000),'and',('they_have_material', shared.player_material)), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._real_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': ba.gettexture("rampageBGColor") + }) + + self.leftwall=ba.newnode('region',attrs={'position': (-17.75152479, 13, -5.52),'scale': (0.1,15.5,2),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + self.rightwall=ba.newnode('region',attrs={'position': (17.75, 13, -5.52),'scale': (0.1,15.5,2),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + self.topwall=ba.newnode('region',attrs={'position': (0, 21.0, -5.52),'scale': (35.4,0.2,2),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + ba.newnode('locator', attrs={'shape':'box', 'position':(-17.75152479, 13, -5.52), 'color':(0,0,0), 'opacity':1,'draw_beauty':True,'additive':False,'size':(0.1,15.5,2)}) + ba.newnode('locator', attrs={'shape':'box', 'position':(17.75, 13, -5.52), 'color':(0,0,0), 'opacity':1,'draw_beauty':True,'additive':False,'size':(0.1,15.5,2)}) + ba.newnode('locator', attrs={'shape':'box', 'position':(0, 21.0, -5.52), 'color':(0,0,0), 'opacity':1,'draw_beauty':True,'additive':False,'size':(35.4,0.2,2)}) + + # self.node_text_left = ba.newnode('text', + # attrs={ + # 'text': "|\n|\n|\n|\n|\n\n\n\n|\n|\n|\n|\n End here \n|\n|\n|\n|\n|\n|\n|\n\n\n\n|\n|\n", + # 'in_world': True, + # 'shadow': 1.0, + # 'flatness': 1.0, + # 'scale':0.019, + # 'h_align': 'center', + # 'position':(-18,20,-5) + # }) + # self.node_text_right = ba.newnode('text', + # attrs={ + # 'text': "|\n|\n|\n|\n|\n\n\n\n|\n|\n|\n|\n End here \n|\n|\n|\n|\n|\n|\n|\n\n\n\n|\n|\n", + # 'in_world': True, + # 'shadow': 1.0, + # 'flatness': 1.0, + # 'scale':0.019, + # 'h_align': 'center', + # 'position':(17,20,-5) + # }) + # self.node_text_top = ba.newnode('text', + # attrs={ + # 'text': "_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _", + # 'in_world': True, + # 'shadow': 1.0, + # 'flatness': 1.0, + # 'scale':0.019, + # 'h_align': 'center', + # 'position':(0,21,-5) + # }) + gnode = ba.getactivity().globalsnode + gnode.happy_thoughts_mode = True + gnode.shadow_offset = (0.0, 8.0, 5.0) + gnode.tint = (1.3, 1.23, 1.0) + gnode.ambient_color = (1.3, 1.23, 1.0) + gnode.vignette_outer = (0.64, 0.59, 0.69) + gnode.vignette_inner = (0.95, 0.95, 0.93) + gnode.vr_near_clip = 1.0 + self.is_flying = True + + # throw out some tips on flying + txt = ba.newnode('text', + attrs={ + 'text': ba.Lstr(resource='pressJumpToFlyText'), + 'scale': 1.2, + 'maxwidth': 800, + 'position': (0, 200), + 'shadow': 0.5, + 'flatness': 0.5, + 'h_align': 'center', + 'v_attach': 'bottom' + }) + cmb = ba.newnode('combine', + owner=txt, + attrs={ + 'size': 4, + 'input0': 0.3, + 'input1': 0.9, + 'input2': 0.0 + }) + ba.animate(cmb, 'input3', {3.0: 0, 4.0: 1, 9.0: 1, 10.0: 0}) + cmb.connectattr('output', txt, 'color') + ba.timer(10.0, txt.delete) + + + +ba._map.register_map(CreativeThoughts) diff --git a/dist/ba_root/mods/maps/soccerStadiumPro.py b/dist/ba_root/mods/maps/soccerStadiumPro.py new file mode 100644 index 0000000..7d9c986 --- /dev/null +++ b/dist/ba_root/mods/maps/soccerStadiumPro.py @@ -0,0 +1,222 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +import ba,_ba +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, List, Dict + +class mapdefs: + points = {} + # noinspection PyDictCreation + boxes = {} + boxes['area_of_interest_bounds'] = (0.0, 1.185751251, 0.4326226188) + ( + 0.0, 0.0, 0.0) + (45.8180273, 11.57249038, 22.89134176) + boxes['edge_box'] = (-0.103873591, 0.4133341891, 0.4294651013) + ( + 0.0, 0.0, 0.0) + (22.48295719, 1.290242794, 8.990252454) + points['ffa_spawn1'] = (-0.08015551329, 0.02275111462, + -4.373674593) + (8.895057015, 1.0, 0.444350722) + points['ffa_spawn2'] = (-0.08015551329, 0.02275111462, + 4.076288941) + (8.895057015, 1.0, 0.444350722) + points['flag1'] = (-10.99027878, 0.05744967453, 0.1095578275) + points['flag2'] = (11.01486398, 0.03986567039, 0.1095578275) + points['flag_default'] = (-0.1001374046, 0.04180340146, 0.1095578275) + boxes['goal1'] = (16.22454533, 1.0, + -1.6087926362) + (0.0, 0.0, 0.0) + (1.6, 4.0, 6.17466313) + boxes['goal2'] = (-16.25961605, 1.0, + -1.6097860203) + (0.0, 0.0, 0.0) + (1.6, 4.0, 6.11856424) + boxes['map_bounds'] = (0.0, 1.185751251, 0.4326226188) + (0.0, 0.0, 0.0) + ( + 42.09506485, 22.81173179, 29.76723155) + points['powerup_spawn1'] = (8.414681236, 0.9515026107, -5.037912441) + points['powerup_spawn2'] = (-8.555402285, 0.9515026107, -5.037912441) + points['powerup_spawn3'] = (5.414681236, 0.9515026107, 5.148223181) + points['powerup_spawn4'] = (-5.737266365, 0.9515026107, 5.148223181) + points['spawn1'] = (-10.03866341, 0.02275111462, 0.0) + (0.5, 1.0, 4.0) + points['spawn2'] = (9.823107149, 0.01092306765, 0.0) + (0.5, 1.0, 4.0) + points['tnt1'] = (-0.08421587483, 0.9515026107, -0.7762602271) +class SoccerStadiumPro(ba.Map): + """Stadium map for football games.""" + defs = mapdefs + + name = 'Soccer Stadium Pro' + + @classmethod + def get_play_types(cls) -> list[str]: + """Return valid play types for this map.""" + return ['melee', 'football', 'team_flag', 'keep_away'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'footballStadiumPreview' + + @classmethod + def on_preload(cls) -> Any: + data: dict[str, Any] = { + 'model': ba.getmodel('footballStadium'), + 'vr_fill_model': ba.getmodel('footballStadiumVRFill'), + 'collide_model': ba.getcollidemodel('footballStadiumCollide'), + 'tex': ba.gettexture('footballStadium') + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + # self.node = ba.newnode( + # 'terrain', + # delegate=self, + # attrs={ + # 'model': self.preloaddata['model'], + # 'collide_model': self.preloaddata['collide_model'], + # 'color_texture': self.preloaddata['tex'], + # 'materials': [shared.footing_material] + # }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_model'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['tex'] + }) + + gnode = ba.getactivity().globalsnode + gnode.tint = (1.3, 1.2, 1.0) + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + # gnode.area_of_interest_bounds=(-20,-2,-10,54,2,3) + self.extend() + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + box_position = self.defs.boxes['edge_box'][0:3] + box_scale = self.defs.boxes['edge_box'][6:9] + xpos = (point.x - box_position[0]) / box_scale[0] + zpos = (point.z - box_position[2]) / box_scale[2] + return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 + + def extend(self): + + shared = SharedObjects.get() + self.mat = ba.Material() + self._real_wall_material=ba.Material() + + self._real_wall_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.mat.add_actions( + + actions=( ('modify_part_collision','physical',False), + ('modify_part_collision','collide',False)) + ) + fakemat=ba.Material() + fakemat.add_actions( + + actions=( ('modify_part_collision','physical',False), + ('modify_part_collision','collide',False)) + ) + # map + pos=(0,0.1,-2) + self.main_ground=ba.newnode('region',attrs={'position': pos,'scale': (54,0.001,28),'type': 'box','materials': [self._real_wall_material,shared.footing_material]}) + self.node_map = ba.newnode('prop', + owner=self.main_ground, + attrs={ + 'model':self.preloaddata['model'], + 'light_model':ba.getmodel('powerupSimple'), + 'position':(0,7,0), + 'body':'puck', + 'shadow_size':0.0, + 'velocity':(0,0,0), + 'color_texture':self.preloaddata['tex'], + + 'reflection_scale':[1.5], + 'materials':[self.mat,shared.footing_material], + 'model_scale':1.6, + 'body_scale':1.7, + + 'density':9000000000 + }) + mnode = ba.newnode('math', + owner=self.main_ground, + attrs={ + 'input1': (0, 0.1, 0), + 'operation': 'add' + }) + self.node_map.changerotation(0,0,0) + self.main_ground.connectattr('position', mnode, 'input2') + mnode.connectattr('output', self.node_map, 'position') + self.main_wall_top=ba.newnode('region',attrs={'position': (-4.30,0.1,-10.8),'scale': (54,20,0.1),'type': 'box','materials': [self._real_wall_material,shared.footing_material]}) + self.main_wall_left=ba.newnode('region',attrs={'position': (-21.30,0.1,-4.8),'scale': (1,20,34),'type': 'box','materials': [self._real_wall_material,shared.footing_material]}) + self.main_wall_right=ba.newnode('region',attrs={'position': (21.30,0.1,-4.8),'scale': (1,20,34),'type': 'box','materials': [self._real_wall_material,shared.footing_material]}) + self.main_wall_bottom=ba.newnode('region',attrs={'position': (-4.30,0.1,6.8),'scale': (54,20,0.1),'type': 'box','materials': [self._real_wall_material,shared.footing_material]}) + + # goal posts + pos=(0.0, 3.504164695739746, -1.6) + self.ud_1_r=ba.newnode('region',attrs={'position': pos,'scale': (2,1,2),'type': 'box','materials': [fakemat ]}) + + self.node = ba.newnode('prop', + owner=self.ud_1_r, + attrs={ + 'model':ba.getmodel('hockeyStadiumOuter'), + 'light_model':ba.getmodel('powerupSimple'), + 'position':(2,7,2), + 'body':'puck', + 'shadow_size':0.0, + 'velocity':(0,0,0), + 'color_texture':ba.gettexture('hockeyStadium'), + + 'reflection_scale':[1.5], + 'materials':[self.mat,shared.footing_material], + 'model_scale':1.9, + 'body_scale':1.9, + + 'density':9000000000 + }) + mnode = ba.newnode('math', + owner=self.ud_1_r, + attrs={ + 'input1': (0, -3.4, 0), + 'operation': 'add' + }) + self.node.changerotation(0,0,0) + self.ud_1_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', self.node, 'position') + + # # // goal post collide model + pos=(16.88630542755127, 0.3009839951992035, -5.2) + self.gp_upper_r=ba.newnode('region',attrs={'position': pos,'scale': (3.5,6.5,0.4),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + pos= (16.88630542755127, 0.4209839951992035, 1.83331298828125) + self.gp_lower_r=ba.newnode('region',attrs={'position': pos,'scale': (3.5,6.5,0.4),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + # roof + pos=(16.88630542755127, 3.6009839951992035, -1.63331298828125) + self.gp_roof_r=ba.newnode('region',attrs={'position': pos,'scale': (3.2,0.1,7.2),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + + # back side + pos=(18.4630542755127, 0.5009839951992035, -2.0) + self.gp_back_r=ba.newnode('region',attrs={'position': pos,'scale': (0.2,6,6.7),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + + # Left ============================================================================= + pos=(-16.85874137878418, 0.3002381920814514, -5.2) + self.gp_upper_l=ba.newnode('region',attrs={'position': pos,'scale': (3.5,6.5,0.4),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + pos=(-16.8830542755127, 0.4209839951992035, 1.83331298828125) + self.gp_lower_l=ba.newnode('region',attrs={'position': pos,'scale': (3.5,6.5,0.4),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + + # roof + pos=(-16.88630542755127, 3.6009839951992035, -1.63331298828125) + self.gp_roof_l=ba.newnode('region',attrs={'position': pos,'scale': (3.2,0.1,7.2),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + + # back side + pos=(-18.4630542755127, 0.5009839951992035, -2.0) + self.gp_back_l=ba.newnode('region',attrs={'position': pos,'scale': (0.2,6,6.7),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + +ba._map.register_map(SoccerStadiumPro) diff --git a/dist/ba_root/mods/maps/soccerStadiumPro.so b/dist/ba_root/mods/maps/soccerStadiumPro.so deleted file mode 100644 index e8c686c..0000000 Binary files a/dist/ba_root/mods/maps/soccerStadiumPro.so and /dev/null differ diff --git a/dist/ba_root/mods/maps/zigZagStubbed.py b/dist/ba_root/mods/maps/zigZagStubbed.py new file mode 100644 index 0000000..c9803e1 --- /dev/null +++ b/dist/ba_root/mods/maps/zigZagStubbed.py @@ -0,0 +1,157 @@ + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, List, Dict + +class ZigZagStubbed(ba.Map): + """A very long zig-zaggy map""" + + from bastd.mapdata import zig_zag as defs + + name = 'Zigzag Stubbed' + + @classmethod + def get_play_types(cls) -> list[str]: + """Return valid play types for this map.""" + return [ + 'melee', 'keep_away', 'team_flag', 'conquest', 'king_of_the_hill' + ] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'zigzagPreview' + + @classmethod + def on_preload(cls) -> Any: + data: dict[str, Any] = { + 'model': ba.getmodel('zigZagLevel'), + 'model_bottom': ba.getmodel('zigZagLevelBottom'), + 'model_bg': ba.getmodel('natureBackground'), + 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), + 'collide_model': ba.getcollidemodel('zigZagLevelCollide'), + 'tex': ba.gettexture('zigZagLevelColor'), + 'model_bg_tex': ba.gettexture('natureBackgroundColor'), + 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), + 'railing_collide_model': ba.getcollidemodel('zigZagLevelBumper'), + 'bg_material': ba.Material() + } + data['bg_material'].add_actions(actions=('modify_part_collision', + 'friction', 10.0)) + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['model_bg'], + 'lighting': False, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['model_bottom'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bg_vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + self.bg_collide = ba.newnode('terrain', + attrs={ + 'collide_model': + self.preloaddata['collide_bg'], + 'materials': [ + shared.footing_material, + self.preloaddata['bg_material'], + shared.death_material + ] + }) + + self._real_wall_material=ba.Material() + + self._real_wall_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self._prop_material=ba.Material() + + self._prop_material.add_actions( + + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + + )) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.0, 1.15, 1.15) + gnode.ambient_color = (1.0, 1.15, 1.15) + gnode.vignette_outer = (0.57, 0.59, 0.63) + gnode.vignette_inner = (0.97, 0.95, 0.93) + gnode.vr_camera_offset = (-1.5, 0, 0) + + self.create_ramp(-4.5,-2.4) + self.create_ramp(-4.5,0) + + self.create_ramp(-1.4,-4.7) + self.create_ramp(-1.4,-2.3) + + self.create_ramp(1.5,-2.4) + self.create_ramp(1.5,0) + + def create_ramp(self,x,z): + shared = SharedObjects.get() + self.ud_1_r=ba.newnode('region',attrs={'position': (x,2.45,z),'scale': (2,1,2.5),'type': 'box','materials': [shared.footing_material,self._real_wall_material ]}) + + self.floor = ba.newnode('prop', + owner=self.ud_1_r, + attrs={ + 'model':ba.getmodel('image1x1'), + 'light_model':ba.getmodel('powerupSimple'), + 'position':(2,7,2), + 'body':'puck', + 'shadow_size':0.0, + 'velocity':(0,0,0), + 'color_texture':ba.gettexture('tnt'), + 'model_scale':2.45, + 'reflection_scale':[.5], + 'materials':[ self._prop_material], + + 'density':9000000000 + }) + mnode = ba.newnode('math', + owner=self.ud_1_r, + attrs={ + 'input1': (0, 0.6, 0), + 'operation': 'add' + }) + + + + self.ud_1_r.connectattr('position', mnode, 'input2') + mnode.connectattr('output', self.floor, 'position') +ba._map.register_map(ZigZagStubbed) \ No newline at end of file diff --git a/dist/ba_root/mods/plugins/auto_stunt.py b/dist/ba_root/mods/plugins/auto_stunt.py new file mode 100644 index 0000000..b657e08 --- /dev/null +++ b/dist/ba_root/mods/plugins/auto_stunt.py @@ -0,0 +1,536 @@ +# ba_meta require api 7 +# AutoStunt mod by - Mr.Smoothy x Rikko +# https://discord.gg/ucyaesh +# https://bombsquad.ga +# Dont modify redistribute this plugin , if want to use features of this plugin in your mod write logic in seprate file +# and import this as module. +# If want to contribute in this original module, raise PR on github https://github.com/bombsquad-community/plugin-manager + +import ba +import _ba +import bastd +from bastd.actor.text import Text +from bastd.actor.image import Image +from bastd.actor import spaz +from bastd.actor import playerspaz +from bastd.gameutils import SharedObjects +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.actor.spazfactory import SpazFactory +from bastd.game.elimination import EliminationGame +import math +import json +import os + +from typing import Optional + +CONTROLS_CENTER = (0, 0) +CONTROLS_SCALE = 1 + +BASE_STUNTS_DIRECTORY = os.path.join(_ba.env()["python_directory_user"], "CustomStunts") +PLAYERS_STUNT_INFO = {} + +STUNT_CACHE = {} +original_on_begin = ba._activity.Activity.on_begin +original_chatmessage = _ba.chatmessage + + +class ControlsUI: + + def on_jump_press(activity): + activity._jump_image.node.color = list( + channel * 2 for channel in activity._jump_image.node.color[:3]) + [1] + + def on_jump_release(activity): + activity._jump_image.node.color = list( + channel * 0.5 for channel in activity._jump_image.node.color[:3]) + [1] + + def on_pickup_press(activity): + activity._pickup_image.node.color = list( + channel * 2 for channel in activity._pickup_image.node.color[:3]) + [1] + + def on_pickup_release(activity): + activity._pickup_image.node.color = list( + channel * 0.5 for channel in activity._pickup_image.node.color[:3]) + [1] + + def on_punch_press(activity): + activity._punch_image.node.color = list( + channel * 2 for channel in activity._punch_image.node.color[:3]) + [1] + + def on_punch_release(activity): + activity._punch_image.node.color = list( + channel * 0.5 for channel in activity._punch_image.node.color[:3]) + [1] + + def on_bomb_press(activity): + activity._bomb_image.node.color = list( + channel * 2 for channel in activity._bomb_image.node.color[:3]) + [1] + + def on_bomb_release(activity): + activity._bomb_image.node.color = list( + channel * 0.5 for channel in activity._bomb_image.node.color[:3]) + [1] + + def on_move_ud(activity, value): + activity.set_stick_image_position(activity, x=activity.stick_image_position_x, y=value) + + def on_move_lr(activity, value): + activity.set_stick_image_position(activity, x=value, y=activity.stick_image_position_y) + + def display(activity): + activity._jump_image.node.color = list(activity._jump_image.node.color[:3]) + [1] + activity._pickup_image.node.color = list(activity._pickup_image.node.color[:3]) + [1] + activity._punch_image.node.color = list(activity._punch_image.node.color[:3]) + [1] + activity._bomb_image.node.color = list(activity._bomb_image.node.color[:3]) + [1] + activity._stick_base_image.opacity = 1.0 + activity._stick_nub_image.opacity = 1.0 + + def hide(activity): + activity._jump_image.node.color = list(activity._jump_image.node.color[:3]) + [0] + activity._pickup_image.node.color = list(activity._pickup_image.node.color[:3]) + [0] + activity._punch_image.node.color = list(activity._punch_image.node.color[:3]) + [0] + activity._bomb_image.node.color = list(activity._bomb_image.node.color[:3]) + [0] + activity._stick_base_image.opacity = 0.0 + activity._stick_nub_image.opacity = 0.0 + + +CONTROLS_UI_MAP = { + "JUMP_PRESS": ControlsUI.on_jump_press, + "JUMP_RELEASE": ControlsUI.on_jump_release, + "PICKUP_PRESS": ControlsUI.on_pickup_press, + "PICKUP_RELEASE": ControlsUI.on_pickup_release, + "PUNCH_PRESS": ControlsUI.on_punch_press, + "PUNCH_RELEASE": ControlsUI.on_punch_release, + "BOMB_PRESS": ControlsUI.on_bomb_press, + "BOMB_RELEASE": ControlsUI.on_bomb_release, + "UP_DOWN": ControlsUI.on_move_ud, + "LEFT_RIGHT": ControlsUI.on_move_lr +} + + +class NewSpaz(bastd.actor.spaz.Spaz): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.move_map = { + "UP_DOWN": self.on_move_up_down, + "LEFT_RIGHT": self.on_move_left_right, + "HOLD_POSITION": self.on_hold_position_press, + "HOLD_RELEASE": self.on_hold_position_release, + "JUMP_PRESS": self.on_jump_press, + "JUMP_RELEASE": self.on_jump_release, + "PICKUP_PRESS": self.on_pickup_press, + "PICKUP_RELEASE": self.on_pickup_release, + "PUNCH_PRESS": self.on_punch_press, + "PUNCH_RELEASE": self.on_punch_release, + "BOMB_PRESS": self.on_bomb_press, + "BOMB_RELEASE": self.on_bomb_release, + "RUN": self.on_run, + } + + +class NewPlayerSpaz(bastd.actor.playerspaz.PlayerSpaz): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.move_map = { + "UP_DOWN": self.on_move_up_down, + "LEFT_RIGHT": self.on_move_left_right, + "HOLD_POSITION": self.on_hold_position_press, + "HOLD_RELEASE": self.on_hold_position_release, + "JUMP_PRESS": self.on_jump_press, + "JUMP_RELEASE": self.on_jump_release, + "PICKUP_PRESS": self.on_pickup_press, + "PICKUP_RELEASE": self.on_pickup_release, + "PUNCH_PRESS": self.on_punch_press, + "PUNCH_RELEASE": self.on_punch_release, + "BOMB_PRESS": self.on_bomb_press, + "BOMB_RELEASE": self.on_bomb_release, + "RUN": self.on_run, + } + self.mirror_spaz = [] + self.source_player.in_replay = False + self.source_player.mirror_mode = False + + def _handle_action(self, action, value: Optional[float] = None) -> None: + if self.source_player.sessionplayer in PLAYERS_STUNT_INFO: + PLAYERS_STUNT_INFO[self.source_player.sessionplayer].append({ + "time": ba.time() - self.source_player.recording_start_time, + "move": { + "action": action, + "value": value, + } + }) + elif self.source_player.in_replay: + ui_activation = CONTROLS_UI_MAP.get(action) + if ui_activation: + if action in ["UP_DOWN", "LEFT_RIGHT"]: + ui_activation(self.source_player.actor._activity(), value) + else: + ui_activation(self.source_player.actor._activity()) + elif self.source_player.mirror_mode: + for mspaz in self.mirror_spaz: + if mspaz and mspaz.node.exists(): + if action in ["UP_DOWN", "LEFT_RIGHT", "RUN"]: + mspaz.move_map[action](value) + else: + mspaz.move_map[action]() + + def on_move_up_down(self, value: float, *args, **kwargs) -> None: + self._handle_action("UP_DOWN", value) + super().on_move_up_down(value, *args, **kwargs) + + def on_move_left_right(self, value: float, *args, **kwargs) -> None: + self._handle_action("LEFT_RIGHT", value) + super().on_move_left_right(value, *args, **kwargs) + + def on_hold_position_press(self, *args, **kwargs) -> None: + self._handle_action("HOLD_POSITION") + super().on_hold_position_press(*args, **kwargs) + + def on_hold_position_release(self, *args, **kwargs) -> None: + self._handle_action("HOLD_RELEASE") + super().on_hold_position_release(*args, **kwargs) + + def on_jump_press(self, *args, **kwargs) -> None: + self._handle_action("JUMP_PRESS") + super().on_jump_press(*args, **kwargs) + + def on_jump_release(self, *args, **kwargs) -> None: + self._handle_action("JUMP_RELEASE") + super().on_jump_release(*args, **kwargs) + + def on_pickup_press(self, *args, **kwargs) -> None: + self._handle_action("PICKUP_PRESS") + super().on_pickup_press(*args, **kwargs) + + def on_pickup_release(self, *args, **kwargs) -> None: + self._handle_action("PICKUP_RELEASE") + super().on_pickup_release(*args, **kwargs) + + def on_punch_press(self, *args, **kwargs) -> None: + self._handle_action("PUNCH_PRESS") + super().on_punch_press(*args, **kwargs) + + def on_punch_release(self, *args, **kwargs) -> None: + self._handle_action("PUNCH_RELEASE") + super().on_punch_release(*args, **kwargs) + + def on_bomb_press(self, *args, **kwargs) -> None: + self._handle_action("BOMB_PRESS") + super().on_bomb_press(*args, **kwargs) + + def on_bomb_release(self, *args, **kwargs) -> None: + self._handle_action("BOMB_RELEASE") + super().on_bomb_release(*args, **kwargs) + + def on_run(self, value: float, *args, **kwargs) -> None: + self._handle_action("RUN", value) + super().on_run(value, *args, **kwargs) + + +def handle_player_replay_end(player): + player.in_replay = False + ControlsUI.hide(player.actor._activity()) + + +def get_player_from_client_id(client_id, activity=None): + activity = activity or _ba.get_foreground_host_activity() + for player in activity.players: + if player.sessionplayer.inputdevice.client_id == client_id: + return player + raise ba.SessionPlayerNotFound() + + +def mirror(clieid): + player = get_player_from_client_id(clieid) + spawn_mirror_spaz(player) + + +def capture(player): + with ba.Context(player.actor._activity()): + player.recording_start_time = ba.time() + PLAYERS_STUNT_INFO[player.sessionplayer] = [] + + +def save(player, stunt_name): + stunt_path = f"{os.path.join(BASE_STUNTS_DIRECTORY, stunt_name)}.json" + os.makedirs(BASE_STUNTS_DIRECTORY, exist_ok=True) + with open(stunt_path, "w") as fout: + json.dump(PLAYERS_STUNT_INFO[player.sessionplayer], fout, indent=2) + del PLAYERS_STUNT_INFO[player.sessionplayer] + + +def replay(player, stunt_name): + stunt_path = f"{os.path.join(BASE_STUNTS_DIRECTORY, stunt_name)}.json" + if stunt_name in STUNT_CACHE: + stunt = STUNT_CACHE[stunt_name] + else: + try: + with open(stunt_path, "r") as fin: + stunt = json.load(fin) + STUNT_CACHE[stunt_name] = stunt + except: + ba.screenmessage(f"{stunt_name} doesn't exists") + return + player.in_replay = True + with ba.Context(player.actor._activity()): + ControlsUI.display(player.actor._activity()) + for move in stunt: + value = move["move"]["value"] + if value is None: + ba.timer( + move["time"], + ba.Call(player.actor.move_map[move["move"]["action"]]) + ) + else: + ba.timer( + move["time"], + ba.Call(player.actor.move_map[move["move"]["action"]], move["move"]["value"]) + ) + last_move_time = move["time"] + time_to_hide_controls = last_move_time + 1 + ba.timer(time_to_hide_controls, ba.Call(handle_player_replay_end, player)) + + +def spawn_mirror_spaz(player): + player.mirror_mode = True + with ba.Context(player.actor._activity()): + bot = spaz.Spaz(player.color, player.highlight, character=player.character).autoretain() + bot.handlemessage(ba.StandMessage( + (player.actor.node.position[0], player.actor.node.position[1], player.actor.node.position[2]+1), 93)) + bot.node.name = player.actor.node.name + bot.node.name_color = player.actor.node.name_color + player.actor.mirror_spaz.append(bot) + + +def ghost(player, stunt_name): + stunt_path = f"{os.path.join(BASE_STUNTS_DIRECTORY, stunt_name)}.json" + if stunt_name in STUNT_CACHE: + stunt = STUNT_CACHE[stunt_name] + else: + try: + with open(stunt_path, "r") as fin: + stunt = json.load(fin) + STUNT_CACHE[stunt_name] = stunt + except: + ba.screenmessage(f"{stunt_name} doesn't exists") + return + player.in_replay = True + + with ba.Context(player.actor._activity()): + bot = spaz.Spaz((1, 0, 0), character="Spaz").autoretain() + bot.handlemessage(ba.StandMessage(player.actor.node.position, 93)) + give_ghost_power(bot) + ControlsUI.display(player.actor._activity()) + for move in stunt: + value = move["move"]["value"] + if value is None: + ba.timer( + move["time"], + ba.Call(bot.move_map[move["move"]["action"]]) + ) + ui_activation = CONTROLS_UI_MAP.get(move["move"]["action"]) + if ui_activation: + ba.timer( + move["time"], + ba.Call(ui_activation, player.actor._activity()) + ) + else: + ba.timer( + move["time"], + ba.Call(bot.move_map[move["move"]["action"]], move["move"]["value"]) + ) + ui_activation = CONTROLS_UI_MAP.get(move["move"]["action"]) + + if ui_activation: + ba.timer( + move["time"], + ba.Call(ui_activation, player.actor._activity(), move["move"]["value"]) + ) + last_move_time = move["time"] + time_to_hide_controls = last_move_time + 1 + ba.timer(time_to_hide_controls, ba.Call(handle_player_replay_end, player)) + ba.timer(time_to_hide_controls, ba.Call(bot.node.delete)) + + +def give_ghost_power(spaz): + spaz.node.invincible = True + shared = SharedObjects.get() + factory = SpazFactory.get() + ghost = ba.Material() + # smoothy hecks + ghost.add_actions( + conditions=(('they_have_material', factory.spaz_material), 'or', + ('they_have_material', shared.player_material), 'or', + ('they_have_material', shared.attack_material), 'or', + ('they_have_material', shared.pickup_material), 'or', + ('they_have_material', PowerupBoxFactory.get().powerup_accept_material)), + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + )) + mats = list(spaz.node.materials) + roller = list(spaz.node.roller_materials) + ext = list(spaz.node.extras_material) + pick = list(spaz.node.pickup_materials) + punch = list(spaz.node.punch_materials) + + mats.append(ghost) + roller.append(ghost) + ext.append(ghost) + pick.append(ghost) + punch.append(ghost) + + spaz.node.materials = tuple(mats) + spaz.node.roller_materials = tuple(roller) + spaz.node.extras_material = tuple(ext) + spaz.node.pickup_materials = tuple(pick) + spaz.node.punch_materials = tuple(pick) + + +def new_chatmessage(msg): + if not msg.startswith("*"): + return original_chatmessage(msg) + + stripped_msg = msg[1:] + msg_splits = stripped_msg.split(maxsplit=3) + command = msg_splits[0] + + client_id = -1 + player = get_player_from_client_id(client_id) + + if command == "start": + capture(player) + _ba.chatmessage("Recording started for {}.".format( + player.getname(), + )) + return original_chatmessage(msg) + + stunt_name = " ".join(msg_splits[1:]) + + if command == "save": + if len(msg_splits) < 2: + ba.screenmessage("Enter name of stunt eg : *save bombjump") + return original_chatmessage(msg) + save(player, stunt_name) + _ba.chatmessage('Recording "{}" by {} saved.'.format( + stunt_name, + player.getname(), + )) + elif command == "stunt": + if len(msg_splits) < 2: + ba.screenmessage("Enter name of stunt eg : *stunt bombjump") + return original_chatmessage(msg) + replay(player, stunt_name) + _ba.chatmessage('Replaying "{}" on {}.'.format( + stunt_name, + player.getname(), + )) + elif command == "learn": + if len(msg_splits) < 2: + ba.screenmessage("Enter name of stunt eg : *learn bombjump") + return original_chatmessage(msg) + ghost(player, stunt_name) + _ba.chatmessage('Replaying "{}" on {}.'.format( + stunt_name, + player.getname(), + )) + elif command == "mirror": + spawn_mirror_spaz(player) + return original_chatmessage(msg) + + +def set_stick_image_position(self, x: float, y: float) -> None: + + # Clamp this to a circle. + len_squared = x * x + y * y + if len_squared > 1.0: + length = math.sqrt(len_squared) + mult = 1.0 / length + x *= mult + y *= mult + + self.stick_image_position_x = x + self.stick_image_position_y = y + offs = 50.0 + assert self._scale is not None + p = [ + self._stick_nub_position[0] + x * offs * 0.6, + self._stick_nub_position[1] + y * offs * 0.6 + ] + c = list(self._stick_nub_image_color) + if abs(x) > 0.1 or abs(y) > 0.1: + c[0] *= 2.0 + c[1] *= 4.0 + c[2] *= 2.0 + assert self._stick_nub_image is not None + self._stick_nub_image.position = p + self._stick_nub_image.color = c + c = list(self._stick_base_image_color) + if abs(x) > 0.1 or abs(y) > 0.1: + c[0] *= 1.5 + c[1] *= 1.5 + c[2] *= 1.5 + assert self._stick_base_image is not None + self._stick_base_image.color = c + + +def on_begin(self, *args, **kwargs) -> None: + self._jump_image = Image( + ba.gettexture('buttonJump'), + position=(385, 160), + scale=(50, 50), + color=[0.1, 0.45, 0.1, 0] + ) + self._pickup_image = Image( + ba.gettexture('buttonPickUp'), + position=(385, 240), + scale=(50, 50), + color=[0, 0.35, 0, 0] + ) + self._punch_image = Image( + ba.gettexture('buttonPunch'), + position=(345, 200), + scale=(50, 50), + color=[0.45, 0.45, 0, 0] + ) + self._bomb_image = Image( + ba.gettexture('buttonBomb'), + position=(425, 200), + scale=(50, 50), + color=[0.45, 0.1, 0.1, 0] + ) + self.stick_image_position_x = self.stick_image_position_y = 0.0 + self._stick_base_position = p = (-328, 200) + self._stick_base_image_color = c2 = (0.25, 0.25, 0.25, 1.0) + self._stick_base_image = ba.newnode( + 'image', + attrs={ + 'texture': ba.gettexture('nub'), + 'absolute_scale': True, + 'vr_depth': -40, + 'position': p, + 'scale': (220.0*0.6, 220.0*0.6), + 'color': c2 + }) + self._stick_nub_position = p = (-328, 200) + self._stick_nub_image_color = c3 = (0.4, 0.4, 0.4, 1.0) + self._stick_nub_image = ba.newnode('image', + attrs={ + 'texture': ba.gettexture('nub'), + 'absolute_scale': True, + 'position': p, + 'scale': (110*0.6, 110*0.66), + 'color': c3 + }) + self._stick_base_image.opacity = 0.0 + self._stick_nub_image.opacity = 0.0 + self.set_stick_image_position = set_stick_image_position + return original_on_begin(self, *args, **kwargs) + + + + +ba._activity.Activity.on_begin = on_begin + # _ba.chatmessage = new_chatmessage +bastd.actor.playerspaz.PlayerSpaz = NewPlayerSpaz +bastd.actor.spaz.Spaz = NewSpaz + + +# lets define a sample elimination game that can use super power of this plugin diff --git a/dist/ba_root/mods/setting.json b/dist/ba_root/mods/setting.json index c8da5d8..6a303c5 100644 --- a/dist/ba_root/mods/setting.json +++ b/dist/ba_root/mods/setting.json @@ -19,6 +19,10 @@ ] } }, + "ScoreScreenAnnouncement":{ + "enable": true, + "msg": ["click stats button to join discord", "watch hey smoothy youtube channel","downlaod new mods from discord"] + }, "statsResetAfterDays":31, "leaderboard":{ "enable":true, @@ -33,11 +37,13 @@ }, "colorfullMap":true, "playlists":{ - "default":12345, "team":12345, - "ffa":22222, - "elim":34343, - "death":23432 + "ffa":412175, + "elim":412172, + "soccer":412160, + "smash":412151, + "ffasmash":412179, + "epic":412173 }, "coopModeWithLessPlayers":{ "enable":false, diff --git a/dist/ba_root/mods/tools/ServerUpdate.py b/dist/ba_root/mods/tools/ServerUpdate.py index 40b33da..6011b6b 100644 --- a/dist/ba_root/mods/tools/ServerUpdate.py +++ b/dist/ba_root/mods/tools/ServerUpdate.py @@ -6,7 +6,7 @@ from efro.terminal import Clr import json import requests import _ba -VERSION=71 +VERSION=75 def check():