Source code for bascenev1lib.game.thelaststand

# Released under the MIT License. See LICENSE for details.
#
"""Defines the last stand minigame."""

from __future__ import annotations

import random
import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, override

import bascenev1 as bs

from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.bomb import TNTSpawner
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.powerupbox import PowerupBoxFactory, PowerupBox
from bascenev1lib.actor.spazbot import (
    SpazBotSet,
    SpazBotDiedMessage,
    BomberBot,
    BomberBotPro,
    BomberBotProShielded,
    BrawlerBot,
    BrawlerBotPro,
    BrawlerBotProShielded,
    TriggerBot,
    TriggerBotPro,
    TriggerBotProShielded,
    ChargerBot,
    StickyBot,
    ExplodeyBot,
)

if TYPE_CHECKING:
    from typing import Any, Sequence
    from bascenev1lib.actor.spazbot import SpazBot


[docs] @dataclass class SpawnInfo: """Spawning info for a particular bot type.""" spawnrate: float increase: float dincrease: float
[docs] class Player(bs.Player['Team']): """Our player type for this game."""
[docs] class Team(bs.Team[Player]): """Our team type for this game."""
[docs] class TheLastStandGame(bs.CoopGameActivity[Player, Team]): """Slow motion how-long-can-you-last game.""" name = 'The Last Stand' description = 'Final glorious epic slow motion battle to the death.' tips = [ 'This level never ends, but a high score here\n' 'will earn you eternal respect throughout the world.' ] # Show messages when players die since it matters here. announce_player_deaths = True # And of course the most important part. slow_motion = True default_music = bs.MusicType.EPIC def __init__(self, settings: dict): settings['map'] = 'Rampage' super().__init__(settings) self._new_wave_sound = bs.getsound('scoreHit01') self._winsound = bs.getsound('score') self._cashregistersound = bs.getsound('cashRegister') self._spawn_center = (0, 5.5, -4.14) self._tntspawnpos = (0, 5.5, -6) self._powerup_center = (0, 7, -4.14) self._powerup_spread = (7, 2) self._preset = str(settings.get('preset', 'default')) self._excludepowerups: list[str] = [] self._scoreboard: Scoreboard | None = None self._score = 0 self._bots = SpazBotSet() self._dingsound = bs.getsound('dingSmall') self._dingsoundhigh = bs.getsound('dingSmallHigh') self._tntspawner: TNTSpawner | None = None self._bot_update_interval: float | None = None self._bot_update_timer: bs.Timer | None = None self._powerup_drop_timer = None # For each bot type: [spawnrate, increase, d_increase] self._bot_spawn_types = { BomberBot: SpawnInfo(1.00, 0.00, 0.000), BomberBotPro: SpawnInfo(0.00, 0.05, 0.001), BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002), BrawlerBot: SpawnInfo(1.00, 0.00, 0.000), BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001), BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), TriggerBot: SpawnInfo(0.30, 0.00, 0.000), TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001), TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), ChargerBot: SpawnInfo(0.30, 0.05, 0.000), StickyBot: SpawnInfo(0.10, 0.03, 0.001), ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002), }
[docs] @override def on_transition_in(self) -> None: super().on_transition_in() bs.timer(1.3, self._new_wave_sound.play) self._scoreboard = Scoreboard( label=bs.Lstr(resource='scoreText'), score_split=0.5 )
[docs] @override def on_begin(self) -> None: super().on_begin() # Spit out a few powerups and start dropping more shortly. self._drop_powerups(standard_points=True) bs.timer(2.0, bs.WeakCall(self._start_powerup_drops)) bs.timer(0.001, bs.WeakCall(self._start_bot_updates)) self.setup_low_life_warning_sound() self._update_scores() self._tntspawner = TNTSpawner( position=self._tntspawnpos, respawn_time=10.0 )
[docs] @override def spawn_player(self, player: Player) -> bs.Actor: pos = ( self._spawn_center[0] + random.uniform(-1.5, 1.5), self._spawn_center[1], self._spawn_center[2] + random.uniform(-1.5, 1.5), ) return self.spawn_player_spaz(player, position=pos)
def _start_bot_updates(self) -> None: self._bot_update_interval = 3.3 - 0.3 * (len(self.players)) self._update_bots() self._update_bots() if len(self.players) > 2: self._update_bots() if len(self.players) > 3: self._update_bots() self._bot_update_timer = bs.Timer( self._bot_update_interval, bs.WeakCall(self._update_bots) ) def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: if poweruptype is None: poweruptype = PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._excludepowerups ) PowerupBox( position=self.map.powerup_spawn_points[index], poweruptype=poweruptype, ).autoretain() def _start_powerup_drops(self) -> None: self._powerup_drop_timer = bs.Timer( 3.0, bs.WeakCall(self._drop_powerups), repeat=True ) def _drop_powerups( self, standard_points: bool = False, force_first: str | None = None ) -> None: """Generic powerup drop.""" from bascenev1lib.actor import powerupbox if standard_points: pts = self.map.powerup_spawn_points for i in range(len(pts)): bs.timer( 1.0 + i * 0.5, bs.WeakCall( self._drop_powerup, i, force_first if i == 0 else None ), ) else: drop_pt = ( self._powerup_center[0] + random.uniform( -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0], ), self._powerup_center[1], self._powerup_center[2] + random.uniform( -self._powerup_spread[1], self._powerup_spread[1] ), ) # Drop one random one somewhere. powerupbox.PowerupBox( position=drop_pt, poweruptype=PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._excludepowerups ), ).autoretain()
[docs] def do_end(self, outcome: str) -> None: """End the game.""" if outcome == 'defeat': self.fade_to_red() self.end( delay=2.0, results={ 'outcome': outcome, 'score': self._score, 'playerinfos': self.initialplayerinfos, }, )
def _update_bots(self) -> None: assert self._bot_update_interval is not None self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98) self._bot_update_timer = bs.Timer( self._bot_update_interval, bs.WeakCall(self._update_bots) ) botspawnpts: list[Sequence[float]] = [ [-5.0, 5.5, -4.14], [0.0, 5.5, -4.14], [5.0, 5.5, -4.14], ] dists = [0.0, 0.0, 0.0] playerpts: list[Sequence[float]] = [] for player in self.players: try: if player.is_alive(): assert isinstance(player.actor, PlayerSpaz) assert player.actor.node playerpts.append(player.actor.node.position) except Exception: logging.exception('Error updating bots.') for i in range(3): for playerpt in playerpts: dists[i] += abs(playerpt[0] - botspawnpts[i][0]) dists[i] += random.random() * 5.0 # Minor random variation. if dists[0] > dists[1] and dists[0] > dists[2]: spawnpt = botspawnpts[0] elif dists[1] > dists[2]: spawnpt = botspawnpts[1] else: spawnpt = botspawnpts[2] spawnpt = ( spawnpt[0] + 3.0 * (random.random() - 0.5), spawnpt[1], 2.0 * (random.random() - 0.5) + spawnpt[2], ) # Normalize our bot type total and find a random number within that. total = 0.0 for spawninfo in self._bot_spawn_types.values(): total += spawninfo.spawnrate randval = random.random() * total # Now go back through and see where this value falls. total = 0 bottype: type[SpazBot] | None = None for spawntype, spawninfo in self._bot_spawn_types.items(): total += spawninfo.spawnrate if randval <= total: bottype = spawntype break spawn_time = 1.0 assert bottype is not None self._bots.spawn_bot(bottype, pos=spawnpt, spawn_time=spawn_time) # After every spawn we adjust our ratios slightly to get more # difficult. for spawninfo in self._bot_spawn_types.values(): spawninfo.spawnrate += spawninfo.increase spawninfo.increase += spawninfo.dincrease def _update_scores(self) -> None: score = self._score # Achievements apply to the default preset only. if self._preset == 'default': if score >= 250: self._award_achievement('Last Stand Master') if score >= 500: self._award_achievement('Last Stand Wizard') if score >= 1000: self._award_achievement('Last Stand God') assert self._scoreboard is not None self._scoreboard.set_team_value(self.teams[0], score, max_score=None)
[docs] @override def handlemessage(self, msg: Any) -> Any: if isinstance(msg, bs.PlayerDiedMessage): player = msg.getplayer(Player) self.stats.player_was_killed(player) bs.timer(0.1, self._checkroundover) elif isinstance(msg, bs.PlayerScoredMessage): self._score += msg.score self._update_scores() elif isinstance(msg, SpazBotDiedMessage): pts, importance = msg.spazbot.get_death_points(msg.how) target: Sequence[float] | None if msg.killerplayer: assert msg.spazbot.node target = msg.spazbot.node.position self.stats.player_scored( msg.killerplayer, pts, target=target, kill=True, screenmessage=False, importance=importance, ) diesound = ( self._dingsound if importance == 1 else self._dingsoundhigh ) diesound.play(volume=0.6) # Normally we pull scores from the score-set, but if there's no # player lets be explicit. else: self._score += pts self._update_scores() else: super().handlemessage(msg)
[docs] @override def end_game(self) -> None: # Tell our bots to celebrate just to rub it in. self._bots.final_celebrate() bs.setmusic(None) bs.pushcall(bs.WeakCall(self.do_end, 'defeat'))
def _checkroundover(self) -> None: """End the round if conditions are met.""" if not any(player.is_alive() for player in self.teams[0].players): self.end_game()