# 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()