# Released under the MIT License. See LICENSE for details.
#
"""Provides Ninja Fight mini-game."""
# ba_meta require api 9
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import random
from typing import TYPE_CHECKING, override
import bascenev1 as bs
from bascenev1lib.actor.spazbot import (
SpazBotSet,
ChargerBot,
SpazBotDiedMessage,
)
from bascenev1lib.actor.onscreentimer import OnScreenTimer
if TYPE_CHECKING:
from typing import Any
[docs]
class Player(bs.Player['Team']):
"""Our player type for this game."""
[docs]
class Team(bs.Team[Player]):
"""Our team type for this game."""
# ba_meta export bascenev1.GameActivity
[docs]
class NinjaFightGame(bs.TeamGameActivity[Player, Team]):
"""
A co-op game where you try to defeat a group
of Ninjas as fast as possible
"""
name = 'Ninja Fight'
description = 'How fast can you defeat the ninjas?'
scoreconfig = bs.ScoreConfig(
label='Time', scoretype=bs.ScoreType.MILLISECONDS, lower_is_better=True
)
default_music = bs.MusicType.TO_THE_DEATH
[docs]
@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
# For now we're hard-coding spawn positions and whatnot
# so we need to be sure to specify that we only support
# a specific map.
return ['Courtyard']
[docs]
@override
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
# We currently support Co-Op only.
return issubclass(sessiontype, bs.CoopSession)
# In the constructor we should load any media we need/etc.
# ...but not actually create anything yet.
def __init__(self, settings: dict):
super().__init__(settings)
self._winsound = bs.getsound('score')
self._won = False
self._timer: OnScreenTimer | None = None
self._bots = SpazBotSet()
self._preset = str(settings['preset'])
# Called when our game actually begins.
[docs]
@override
def on_begin(self) -> None:
super().on_begin()
is_pro = self._preset == 'pro'
# In pro mode there's no powerups.
if not is_pro:
self.setup_standard_powerup_drops()
# Make our on-screen timer and start it roughly when our bots appear.
self._timer = OnScreenTimer()
bs.timer(4.0, self._timer.start)
# Spawn some baddies.
bs.timer(
1.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(3, 3, -2), spawn_time=3.0
),
)
bs.timer(
2.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(-3, 3, -2), spawn_time=3.0
),
)
bs.timer(
3.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(5, 3, -2), spawn_time=3.0
),
)
bs.timer(
4.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(-5, 3, -2), spawn_time=3.0
),
)
# Add some extras for multiplayer or pro mode.
assert self.initialplayerinfos is not None
if len(self.initialplayerinfos) > 2 or is_pro:
bs.timer(
5.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(0, 3, -5), spawn_time=3.0
),
)
if len(self.initialplayerinfos) > 3 or is_pro:
bs.timer(
6.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(0, 3, 1), spawn_time=3.0
),
)
# Called for each spawning player.
[docs]
@override
def spawn_player(self, player: Player) -> bs.Actor:
# (Pylint Bug?) pylint: disable=missing-function-docstring
# Let's spawn close to the center.
spawn_center = (0, 3, -2)
pos = (
spawn_center[0] + random.uniform(-1.5, 1.5),
spawn_center[1],
spawn_center[2] + random.uniform(-1.5, 1.5),
)
return self.spawn_player_spaz(player, position=pos)
def _check_if_won(self) -> None:
# Simply end the game if there's no living bots.
# FIXME: Should also make sure all bots have been spawned;
# if spawning is spread out enough that we're able to kill
# all living bots before the next spawns, it would incorrectly
# count as a win.
if not self._bots.have_living_bots():
self._won = True
self.end_game()
# Called for miscellaneous messages.
[docs]
@override
def handlemessage(self, msg: Any) -> Any:
# (Pylint Bug?) pylint: disable=missing-function-docstring
# A player has died.
if isinstance(msg, bs.PlayerDiedMessage):
super().handlemessage(msg) # Augment standard behavior.
self.respawn_player(msg.getplayer(Player))
# A spaz-bot has died.
elif isinstance(msg, SpazBotDiedMessage):
# Unfortunately the bot-set will always tell us there are living
# bots if we ask here (the currently-dying bot isn't officially
# marked dead yet) ..so lets push a call into the event loop to
# check once this guy has finished dying.
bs.pushcall(self._check_if_won)
# Let the base class handle anything we don't.
else:
return super().handlemessage(msg)
return None
# When this is called, we should fill out results and end the game
# *regardless* of whether is has been won. (this may be called due
# to a tournament ending or other external reason).
[docs]
@override
def end_game(self) -> None:
# (Pylint Bug?) pylint: disable=missing-function-docstring
# Stop our on-screen timer so players can see what they got.
assert self._timer is not None
self._timer.stop()
results = bs.GameResults()
# If we won, set our score to the elapsed time in milliseconds.
# (there should just be 1 team here since this is co-op).
# ..if we didn't win, leave scores as default (None) which means
# we lost.
if self._won:
elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0)
bs.cameraflash()
self._winsound.play()
for team in self.teams:
for player in team.players:
if player.actor:
player.actor.handlemessage(bs.CelebrateMessage())
results.set_team_score(team, elapsed_time_ms)
# Ends the activity.
self.end(results)
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum