"""New Duel / Created by: byANG3L""" # ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations from typing import TYPE_CHECKING import babase import bauiv1 as bui import bascenev1 as bs import _babase import random from bascenev1lib.actor.bomb import Bomb from bascenev1lib.actor.playerspaz import PlayerSpaz from bascenev1lib.actor.onscreentimer import OnScreenTimer if TYPE_CHECKING: pass lang = bs.app.lang.language if lang == 'Spanish': name = 'Infección' description = '¡Se está extendiendo!' instance_description = '¡Evite la propagación!' mines = 'Minas' enable_bombs = 'Habilitar Bombas' extra_mines = 'Seg/Mina Extra' max_infected_size = 'Tamaño Máx. de Infección' max_size_increases = 'Incrementar Tamaño Cada' infection_spread_rate = 'Velocidad de Infección' faster = 'Muy Rápido' fast = 'Rápido' normal = 'Normal' slow = 'Lento' slowest = 'Muy Lento' insane = 'Insano' else: name = 'Infection' description = "It's spreading!" instance_description = 'Avoid the spread!' mines = 'Mines' enable_bombs = 'Enable Bombs' extra_mines = 'Sec/Extra Mine' max_infected_size = 'Max Infected Size' max_size_increases = 'Max Size Increases Every' infection_spread_rate = 'Infection Spread Rate' faster = 'Faster' fast = 'Fast' normal = 'Normal' slow = 'Slow' slowest = 'Slowest' insane = 'Insane' def ba_get_api_version(): return 6 def ba_get_levels(): return [babase._level.Level( name, gametype=Infection, settings={}, preview_texture_name='footballStadiumPreview')] class myMine(Bomb): # reason for the mine class is so we can add the death zone def __init__(self, pos: Sequence[float] = (0.0, 1.0, 0.0)): Bomb.__init__(self, position=pos, bomb_type='land_mine') showInSpace = False self.died = False self.rad = 0.3 self.zone = bs.newnode( 'locator', attrs={ 'shape': 'circle', 'position': self.node.position, 'color': (1, 0, 0), 'opacity': 0.5, 'draw_beauty': showInSpace, 'additive': True}) bs.animate_array( self.zone, 'size', 1, {0: [0.0], 0.05: [2*self.rad]}) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.DieMessage): if not self.died: self.getactivity().mine_count -= 1 self.died = True bs.animate_array( self.zone, 'size', 1, {0: [2*self.rad], 0.05: [0]}) self.zone = None super().handlemessage(msg) else: super().handlemessage(msg) class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: self.survival_seconds: Optional[int] = None self.death_time: Optional[float] = None class Team(bs.Team[Player]): """Our team type for this game.""" # ba_meta export bascenev1.GameActivity class Infection(bs.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[bs.Session]) -> List[babase.Setting]: settings = [ bs.IntSetting( mines, min_value=5, default=10, increment=5, ), bs.BoolSetting(enable_bombs, default=True), bs.IntSetting( extra_mines, min_value=1, default=10, increment=1, ), bs.IntSetting( max_infected_size, min_value=4, default=6, increment=1, ), bs.IntChoiceSetting( max_size_increases, choices=[ ('10s', 10), ('20s', 20), ('30s', 30), ('1 Minute', 60), ], default=20, ), bs.IntChoiceSetting( infection_spread_rate, choices=[ (slowest, 0.01), (slow, 0.02), (normal, 0.03), (fast, 0.04), (faster, 0.05), (insane, 0.08), ], default=0.03, ), bs.BoolSetting('Epic Mode', default=False), ] return settings @classmethod def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool: return (issubclass(sessiontype, bs.CoopSession) or issubclass(sessiontype, bs.MultiTeamSession)) @classmethod def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]: return ['Doom Shroom', 'Rampage', 'Hockey Stadium', 'Crag Castle', 'Big G', 'Football Stadium'] def __init__(self, settings: dict): super().__init__(settings) self.mines: List = [] self._update_rate = 0.1 self._last_player_death_time = None self._start_time: Optional[float] = None self._timer: Optional[OnScreenTimer] = None self._epic_mode = bool(settings['Epic Mode']) self._max_mines = int(settings[mines]) self._extra_mines = int(settings[extra_mines]) self._enable_bombs = bool(settings[enable_bombs]) self._max_size = int(settings[max_infected_size]) self._max_size_increases = float(settings[max_size_increases]) self._growth_rate = float(settings[infection_spread_rate]) # Base class overrides. self.slow_motion = self._epic_mode self.default_music = (bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL) def get_instance_description(self) -> Union[str, Sequence]: return instance_description def get_instance_description_short(self) -> Union[str, Sequence]: return instance_description def on_begin(self) -> None: super().on_begin() self._start_time = bs.time() self.mine_count = 0 bs.timer(self._update_rate, bs.WeakCall(self._mine_update), repeat=True) bs.timer(self._max_size_increases*1.0, bs.WeakCall(self._max_size_update), repeat=True) bs.timer(self._extra_mines*1.0, bs.WeakCall(self._max_mine_update), repeat=True) self._timer = OnScreenTimer() self._timer.start() # Check for immediate end (if we've only got 1 player, etc). bs.timer(5.0, self._check_end_game) def on_player_join(self, player: Player) -> None: if self.has_begun(): assert self._timer is not None player.survival_seconds = self._timer.getstarttime() bs.broadcastmessage( babase.Lstr(resource='playerDelayedJoinText', subs=[('${PLAYER}', player.getname(full=True))]), color=(0, 1, 0), ) return self.spawn_player(player) def on_player_leave(self, player: Player) -> None: super().on_player_leave(player) self._check_end_game() def _max_mine_update(self) -> None: self._max_mines += 1 def _max_size_update(self) -> None: self._max_size += 1 def _mine_update(self) -> None: # print self.mineCount # purge dead mines, update their animantion, check if players died for m in self.mines: if not m.node: self.mines.remove(m) else: # First, check if any player is within the current death zone for player in self.players: if not player.actor is None: if player.actor.is_alive(): p1 = player.actor.node.position p2 = m.node.position diff = (babase.Vec3(p1[0]-p2[0], 0.0, p1[2]-p2[2])) dist = (diff.length()) if dist < m.rad: player.actor.handlemessage(bs.DieMessage()) # Now tell the circle to grow to the new size if m.rad < self._max_size: bs.animate_array( m.zone, 'size', 1, {0: [m.rad*2], self._update_rate: [(m.rad+self._growth_rate)*2]}) # Tell the circle to be the new size. # This will be the new check radius next time. m.rad += self._growth_rate # make a new mine if needed. if self.mine_count < self._max_mines: pos = self.getRandomPowerupPoint() self.mine_count += 1 self._flash_mine(pos) bs.timer(0.95, babase.Call(self._make_mine, pos)) def _make_mine(self, posn: Sequence[float]) -> None: m = myMine(pos=posn) m.arm() self.mines.append(m) def _flash_mine(self, pos: Sequence[float]) -> None: light = bs.newnode('light', attrs={ 'position': pos, 'color': (1, 0.2, 0.2), 'radius': 0.1, 'height_attenuated': False }) bs.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) bs.timer(1.0, light.delete) def end_game(self) -> None: results = bs.GameResults() for team in self.teams: results.set_team_score(team, int(team.survival_seconds)) self.end(results=results, announce_delay=0.8) def _flash_player(self, player: Player, scale: float) -> None: assert isinstance(player.actor, PlayerSpaz) assert player.actor.node pos = player.actor.node.position light = bs.newnode('light', attrs={ 'position': pos, 'color': (1, 1, 0), 'height_attenuated': False, 'radius': 0.4 }) bs.timer(0.5, light.delete) bs.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) death_time = bs.time() msg.getplayer(Player).death_time = death_time if isinstance(self.session, bs.CoopSession): # Teams will still show up if we check now.. check in # the next cycle. babase.pushcall(self._check_end_game) # Also record this for a final setting of the clock. self._last_player_death_time = death_time else: bs.timer(1.0, self._check_end_game) else: # Default handler: return super().handlemessage(msg) return None def _check_end_game(self) -> None: living_team_count = 0 for team in self.teams: for player in team.players: if player.is_alive(): living_team_count += 1 break # In co-op, we go till everyone is dead.. otherwise we go # until one team remains. if isinstance(self.session, bs.CoopSession): if living_team_count <= 0: self.end_game() else: if living_team_count <= 1: self.end_game() def spawn_player(self, player: PlayerType) -> bs.Actor: spaz = self.spawn_player_spaz(player) # Let's reconnect this player's controls to this # spaz but *without* the ability to attack or pick stuff up. spaz.connect_controls_to_player(enable_punch=False, enable_bomb=self._enable_bombs, enable_pickup=False) # Also lets have them make some noise when they die. spaz.play_big_death_sound = True return spaz def spawn_player_spaz(self, player: PlayerType, position: Sequence[float] = (0, 0, 0), angle: float = None) -> PlayerSpaz: """Create and wire up a bs.PlayerSpaz for the provided bs.Player.""" # pylint: disable=too-many-locals # pylint: disable=cyclic-import position = self.map.get_ffa_start_position(self.players) name = player.getname() color = player.color highlight = player.highlight light_color = babase._math.normalized_color(color) display_color = _babase.safecolor(color, target_intensity=0.75) spaz = PlayerSpaz(color=color, highlight=highlight, character=player.character, player=player) player.actor = spaz assert spaz.node # If this is co-op and we're on Courtyard or Runaround, add the # material that allows us to collide with the player-walls. # FIXME: Need to generalize this. if isinstance(self.session, bs.CoopSession) and self.map.getname() in [ 'Courtyard', 'Tower D' ]: mat = self.map.preloaddata['collide_with_wall_material'] assert isinstance(spaz.node.materials, tuple) assert isinstance(spaz.node.roller_materials, tuple) spaz.node.materials += (mat, ) spaz.node.roller_materials += (mat, ) spaz.node.name = name spaz.node.name_color = display_color spaz.connect_controls_to_player() # Move to the stand position and add a flash of light. spaz.handlemessage( bs.StandMessage( position, angle if angle is not None else random.uniform(0, 360))) self._spawn_sound.play(1, position=spaz.node.position) light = bs.newnode('light', attrs={'color': light_color}) spaz.node.connectattr('position', light, 'position') bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) bs.timer(0.5, light.delete) return spaz def getRandomPowerupPoint(self) -> None: # So far, randomized points only figured out for mostly rectangular maps. # Boxes will still fall through holes, but shouldn't be terrible problem (hopefully) # If you add stuff here, need to add to "supported maps" above. # ['Doom Shroom', 'Rampage', 'Hockey Stadium', 'Courtyard', 'Crag Castle', 'Big G', 'Football Stadium'] myMap = self.map.getname() # print(myMap) if myMap == 'Doom Shroom': while True: x = random.uniform(-1.0, 1.0) y = random.uniform(-1.0, 1.0) if x*x+y*y < 1.0: break return ((8.0*x, 2.5, -3.5+5.0*y)) elif myMap == 'Rampage': x = random.uniform(-6.0, 7.0) y = random.uniform(-6.0, -2.5) return ((x, 5.2, y)) elif myMap == 'Hockey Stadium': x = random.uniform(-11.5, 11.5) y = random.uniform(-4.5, 4.5) return ((x, 0.2, y)) elif myMap == 'Courtyard': x = random.uniform(-4.3, 4.3) y = random.uniform(-4.4, 0.3) return ((x, 3.0, y)) elif myMap == 'Crag Castle': x = random.uniform(-6.7, 8.0) y = random.uniform(-6.0, 0.0) return ((x, 10.0, y)) elif myMap == 'Big G': x = random.uniform(-8.7, 8.0) y = random.uniform(-7.5, 6.5) return ((x, 3.5, y)) elif myMap == 'Football Stadium': x = random.uniform(-12.5, 12.5) y = random.uniform(-5.0, 5.5) return ((x, 0.32, y)) else: x = random.uniform(-5.0, 5.0) y = random.uniform(-6.0, 0.0) return ((x, 8.0, y)) def end_game(self) -> None: cur_time = bs.time() assert self._timer is not None start_time = self._timer.getstarttime() # Mark death-time as now for any still-living players # and award players points for how long they lasted. # (these per-player scores are only meaningful in team-games) for team in self.teams: for player in team.players: survived = False # Throw an extra fudge factor in so teams that # didn't die come out ahead of teams that did. if player.death_time is None: survived = True player.death_time = cur_time + 1 # Award a per-player score depending on how many seconds # they lasted (per-player scores only affect teams mode; # everywhere else just looks at the per-team score). score = int(player.death_time - self._timer.getstarttime()) if survived: score += 50 # A bit extra for survivors. self.stats.player_scored(player, score, screenmessage=False) # Stop updating our time text, and set the final time to match # exactly when our last guy died. self._timer.stop(endtime=self._last_player_death_time) # Ok now calc game results: set a score for each team and then tell # the game to end. results = bs.GameResults() # Remember that 'free-for-all' mode is simply a special form # of 'teams' mode where each player gets their own team, so we can # just always deal in teams and have all cases covered. for team in self.teams: # Set the team score to the max time survived by any player on # that team. longest_life = 0.0 for player in team.players: assert player.death_time is not None longest_life = max(longest_life, player.death_time - start_time) # Submit the score value in milliseconds. results.set_team_score(team, int(1000.0 * longest_life)) self.end(results=results)