vh-bombsquad-modded-server-.../dist/ba_data/python/bastd/game/football.py
2024-06-06 19:50:58 +05:30

988 lines
35 KiB
Python

# Released under the MIT License. See LICENSE for details.
#
"""Implements football games (both co-op and teams varieties)."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import math
import ba
from bastd.actor.bomb import TNTSpawner
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.actor.respawnicon import RespawnIcon
from bastd.actor.powerupbox import PowerupBoxFactory, PowerupBox
from bastd.actor.flag import (
FlagFactory,
Flag,
FlagPickedUpMessage,
FlagDroppedMessage,
FlagDiedMessage,
)
from bastd.actor.spazbot import (
SpazBotDiedMessage,
SpazBotPunchedMessage,
SpazBotSet,
BrawlerBotLite,
BrawlerBot,
BomberBotLite,
BomberBot,
TriggerBot,
ChargerBot,
TriggerBotPro,
BrawlerBotPro,
StickyBot,
ExplodeyBot,
)
if TYPE_CHECKING:
from typing import Any, Sequence
from bastd.actor.spaz import Spaz
from bastd.actor.spazbot import SpazBot
class FootballFlag(Flag):
"""Custom flag class for football games."""
def __init__(self, position: Sequence[float]):
super().__init__(
position=position, dropped_timeout=20, color=(1.0, 1.0, 0.3)
)
assert self.node
self.last_holding_player: ba.Player | None = None
self.node.is_area_of_interest = True
self.respawn_timer: ba.Timer | None = None
self.scored = False
self.held_count = 0
self.light = ba.newnode(
'light',
owner=self.node,
attrs={
'intensity': 0.25,
'height_attenuated': False,
'radius': 0.2,
'color': (0.9, 0.7, 0.0),
},
)
self.node.connectattr('position', self.light, 'position')
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
self.respawn_timer: ba.Timer | None = None
self.respawn_icon: RespawnIcon | None = None
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.score = 0
# ba_meta export game
class FootballTeamGame(ba.TeamGameActivity[Player, Team]):
"""Football game for teams mode."""
name = 'Football'
description = 'Get the flag to the enemy end zone.'
available_settings = [
ba.IntSetting(
'Score to Win',
min_value=7,
default=21,
increment=7,
),
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('Epic Mode', default=False),
]
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
# We only support two-team play.
return issubclass(sessiontype, ba.DualTeamSession)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('football')
def __init__(self, settings: dict):
super().__init__(settings)
self._scoreboard: Scoreboard | None = Scoreboard()
# Load some media we need.
self._cheer_sound = ba.getsound('cheer')
self._chant_sound = ba.getsound('crowdChant')
self._score_sound = ba.getsound('score')
self._swipsound = ba.getsound('swip')
self._whistle_sound = ba.getsound('refWhistle')
self._score_region_material = ba.Material()
self._score_region_material.add_actions(
conditions=('they_have_material', FlagFactory.get().flagmaterial),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('call', 'at_connect', self._handle_score),
),
)
self._flag_spawn_pos: Sequence[float] | None = None
self._score_regions: list[ba.NodeActor] = []
self._flag: FootballFlag | None = None
self._flag_respawn_timer: ba.Timer | None = None
self._flag_respawn_light: ba.NodeActor | None = None
self._score_to_win = int(settings['Score to Win'])
self._time_limit = float(settings['Time Limit'])
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.FOOTBALL
)
def get_instance_description(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
# NOTE: if use just touchdowns = self._score_to_win // 7
# and we will need to score, for example, 27 points,
# we will be required to score 3 (not 4) goals ..
touchdowns = math.ceil(touchdowns)
if touchdowns > 1:
return 'Score ${ARG1} touchdowns.', touchdowns
return 'Score a touchdown.'
def get_instance_description_short(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
touchdowns = math.ceil(touchdowns)
if touchdowns > 1:
return 'score ${ARG1} touchdowns', touchdowns
return 'score a touchdown'
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
self._flag_spawn_pos = self.map.get_flag_position(None)
self._spawn_flag()
defs = self.map.defs
self._score_regions.append(
ba.NodeActor(
ba.newnode(
'region',
attrs={
'position': defs.boxes['goal1'][0:3],
'scale': defs.boxes['goal1'][6:9],
'type': 'box',
'materials': (self._score_region_material,),
},
)
)
)
self._score_regions.append(
ba.NodeActor(
ba.newnode(
'region',
attrs={
'position': defs.boxes['goal2'][0:3],
'scale': defs.boxes['goal2'][6:9],
'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 _kill_flag(self) -> None:
self._flag = None
def _handle_score(self) -> None:
"""A point has been scored."""
# Our flag might stick around for a second or two
# make sure it doesn't score again.
assert self._flag is not None
if self._flag.scored:
return
region = ba.getcollision().sourcenode
i = None
for i, score_region in enumerate(self._score_regions):
if region == score_region.node:
break
for team in self.teams:
if team.id == i:
team.score += 7
# Tell all players to celebrate.
for player in team.players:
if player.actor:
player.actor.handlemessage(ba.CelebrateMessage(2.0))
# If someone on this team was last to touch it,
# give them points.
assert self._flag is not None
if (
self._flag.last_holding_player
and team == self._flag.last_holding_player.team
):
self.stats.player_scored(
self._flag.last_holding_player, 50, big_message=True
)
# End the game if we won.
if team.score >= self._score_to_win:
self.end_game()
ba.playsound(self._score_sound)
ba.playsound(self._cheer_sound)
assert self._flag
self._flag.scored = True
# Kill the flag (it'll respawn shortly).
ba.timer(1.0, self._kill_flag)
light = ba.newnode(
'light',
attrs={
'position': ba.getcollision().position,
'height_attenuated': False,
'color': (1, 0, 0),
},
)
ba.animate(light, 'intensity', {0.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, announce_delay=0.8)
def _update_scoreboard(self) -> None:
assert self._scoreboard is not None
for team in self.teams:
self._scoreboard.set_team_value(
team, team.score, self._score_to_win
)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, FlagPickedUpMessage):
assert isinstance(msg.flag, FootballFlag)
try:
msg.flag.last_holding_player = msg.node.getdelegate(
PlayerSpaz, True
).getplayer(Player, True)
except ba.NotFoundError:
pass
msg.flag.held_count += 1
elif isinstance(msg, FlagDroppedMessage):
assert isinstance(msg.flag, FootballFlag)
msg.flag.held_count -= 1
# Respawn dead players if they're still in the game.
elif isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
self.respawn_player(msg.getplayer(Player))
# Respawn dead flags.
elif isinstance(msg, FlagDiedMessage):
if not self.has_ended():
self._flag_respawn_timer = ba.Timer(3.0, self._spawn_flag)
self._flag_respawn_light = ba.NodeActor(
ba.newnode(
'light',
attrs={
'position': self._flag_spawn_pos,
'height_attenuated': False,
'radius': 0.15,
'color': (1.0, 1.0, 0.3),
},
)
)
assert self._flag_respawn_light.node
ba.animate(
self._flag_respawn_light.node,
'intensity',
{0.0: 0, 0.25: 0.15, 0.5: 0},
loop=True,
)
ba.timer(3.0, self._flag_respawn_light.node.delete)
else:
# Augment standard behavior.
super().handlemessage(msg)
def _flash_flag_spawn(self) -> None:
light = ba.newnode(
'light',
attrs={
'position': self._flag_spawn_pos,
'height_attenuated': False,
'color': (1, 1, 0),
},
)
ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True)
ba.timer(1.0, light.delete)
def _spawn_flag(self) -> None:
ba.playsound(self._swipsound)
ba.playsound(self._whistle_sound)
self._flash_flag_spawn()
assert self._flag_spawn_pos is not None
self._flag = FootballFlag(position=self._flag_spawn_pos)
class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
"""Co-op variant of football."""
name = 'Football'
tips = ['Use the pick-up button to grab the flag < ${PICKUP} >']
scoreconfig = ba.ScoreConfig(
scoretype=ba.ScoreType.MILLISECONDS, version='B'
)
default_music = ba.MusicType.FOOTBALL
# FIXME: Need to update co-op games to use getscoreconfig.
def get_score_type(self) -> str:
return 'time'
def get_instance_description(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
touchdowns = math.ceil(touchdowns)
if touchdowns > 1:
return 'Score ${ARG1} touchdowns.', touchdowns
return 'Score a touchdown.'
def get_instance_description_short(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
touchdowns = math.ceil(touchdowns)
if touchdowns > 1:
return 'score ${ARG1} touchdowns', touchdowns
return 'score a touchdown'
def __init__(self, settings: dict):
settings['map'] = 'Football Stadium'
super().__init__(settings)
self._preset = settings.get('preset', 'rookie')
# Load some media we need.
self._cheer_sound = ba.getsound('cheer')
self._boo_sound = ba.getsound('boo')
self._chant_sound = ba.getsound('crowdChant')
self._score_sound = ba.getsound('score')
self._swipsound = ba.getsound('swip')
self._whistle_sound = ba.getsound('refWhistle')
self._score_to_win = 21
self._score_region_material = ba.Material()
self._score_region_material.add_actions(
conditions=('they_have_material', FlagFactory.get().flagmaterial),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('call', 'at_connect', self._handle_score),
),
)
self._powerup_center = (0, 2, 0)
self._powerup_spread = (10, 5.5)
self._player_has_dropped_bomb = False
self._player_has_punched = False
self._scoreboard: Scoreboard | None = None
self._flag_spawn_pos: Sequence[float] | None = None
self._score_regions: list[ba.NodeActor] = []
self._exclude_powerups: list[str] = []
self._have_tnt = False
self._bot_types_initial: list[type[SpazBot]] | None = None
self._bot_types_7: list[type[SpazBot]] | None = None
self._bot_types_14: list[type[SpazBot]] | None = None
self._bot_team: Team | None = None
self._starttime_ms: int | None = None
self._time_text: ba.NodeActor | None = None
self._time_text_input: ba.NodeActor | None = None
self._tntspawner: TNTSpawner | None = None
self._bots = SpazBotSet()
self._bot_spawn_timer: ba.Timer | None = None
self._powerup_drop_timer: ba.Timer | None = None
self._scoring_team: Team | None = None
self._final_time_ms: int | None = None
self._time_text_timer: ba.Timer | None = None
self._flag_respawn_light: ba.Actor | None = None
self._flag: FootballFlag | None = None
def on_transition_in(self) -> None:
super().on_transition_in()
self._scoreboard = Scoreboard()
self._flag_spawn_pos = self.map.get_flag_position(None)
self._spawn_flag()
# Set up the two score regions.
defs = self.map.defs
self._score_regions.append(
ba.NodeActor(
ba.newnode(
'region',
attrs={
'position': defs.boxes['goal1'][0:3],
'scale': defs.boxes['goal1'][6:9],
'type': 'box',
'materials': [self._score_region_material],
},
)
)
)
self._score_regions.append(
ba.NodeActor(
ba.newnode(
'region',
attrs={
'position': defs.boxes['goal2'][0:3],
'scale': defs.boxes['goal2'][6:9],
'type': 'box',
'materials': [self._score_region_material],
},
)
)
)
ba.playsound(self._chant_sound)
def on_begin(self) -> None:
# FIXME: Split this up a bit.
# pylint: disable=too-many-statements
from bastd.actor import controlsguide
super().on_begin()
# Show controls help in kiosk mode.
if ba.app.demo_mode or ba.app.arcade_mode:
controlsguide.ControlsGuide(
delay=3.0, lifespan=10.0, bright=True
).autoretain()
assert self.initialplayerinfos is not None
abot: type[SpazBot]
bbot: type[SpazBot]
cbot: type[SpazBot]
if self._preset in ['rookie', 'rookie_easy']:
self._exclude_powerups = ['curse']
self._have_tnt = False
abot = (
BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot
)
self._bot_types_initial = [abot] * len(self.initialplayerinfos)
bbot = BomberBotLite if self._preset == 'rookie_easy' else BomberBot
self._bot_types_7 = [bbot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
cbot = BomberBot if self._preset == 'rookie_easy' else TriggerBot
self._bot_types_14 = [cbot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
elif self._preset == 'tournament':
self._exclude_powerups = []
self._have_tnt = True
self._bot_types_initial = [BrawlerBot] * (
1 if len(self.initialplayerinfos) < 2 else 2
)
self._bot_types_7 = [TriggerBot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
self._bot_types_14 = [ChargerBot] * (
1 if len(self.initialplayerinfos) < 4 else 2
)
elif self._preset in ['pro', 'pro_easy', 'tournament_pro']:
self._exclude_powerups = ['curse']
self._have_tnt = True
self._bot_types_initial = [ChargerBot] * len(
self.initialplayerinfos
)
abot = BrawlerBot if self._preset == 'pro' else BrawlerBotLite
typed_bot_list: list[type[SpazBot]] = []
self._bot_types_7 = (
typed_bot_list
+ [abot]
+ [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2)
)
bbot = TriggerBotPro if self._preset == 'pro' else TriggerBot
self._bot_types_14 = [bbot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
elif self._preset in ['uber', 'uber_easy']:
self._exclude_powerups = []
self._have_tnt = True
abot = BrawlerBotPro if self._preset == 'uber' else BrawlerBot
bbot = TriggerBotPro if self._preset == 'uber' else TriggerBot
typed_bot_list_2: list[type[SpazBot]] = []
self._bot_types_initial = (
typed_bot_list_2
+ [StickyBot]
+ [abot] * len(self.initialplayerinfos)
)
self._bot_types_7 = [bbot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
self._bot_types_14 = [ExplodeyBot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
else:
raise Exception()
self.setup_low_life_warning_sound()
self._drop_powerups(standard_points=True)
ba.timer(4.0, self._start_powerup_drops)
# Make a bogus team for our bots.
bad_team_name = self.get_team_display_string('Bad Guys')
self._bot_team = Team()
self._bot_team.manual_init(
team_id=1, name=bad_team_name, color=(0.5, 0.4, 0.4)
)
for team in [self.teams[0], self._bot_team]:
team.score = 0
self.update_scores()
# Time display.
starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
assert isinstance(starttime_ms, int)
self._starttime_ms = starttime_ms
self._time_text = ba.NodeActor(
ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': 'center',
'h_align': 'center',
'color': (1, 1, 0.5, 1),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, -50),
'scale': 1.3,
'text': '',
},
)
)
self._time_text_input = ba.NodeActor(
ba.newnode('timedisplay', attrs={'showsubseconds': True})
)
self.globalsnode.connectattr(
'time', self._time_text_input.node, 'time2'
)
assert self._time_text_input.node
assert self._time_text.node
self._time_text_input.node.connectattr(
'output', self._time_text.node, 'text'
)
# Our TNT spawner (if applicable).
if self._have_tnt:
self._tntspawner = TNTSpawner(position=(0, 1, -1))
self._bots = SpazBotSet()
self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True)
for bottype in self._bot_types_initial:
self._spawn_bot(bottype)
def _on_bot_spawn(self, spaz: SpazBot) -> None:
# We want to move to the left by default.
spaz.target_point_default = ba.Vec3(0, 0, 0)
def _spawn_bot(
self, spaz_type: type[SpazBot], immediate: bool = False
) -> None:
assert self._bot_team is not None
pos = self.map.get_start_position(self._bot_team.id)
self._bots.spawn_bot(
spaz_type,
pos=pos,
spawn_time=0.001 if immediate else 3.0,
on_spawn_call=self._on_bot_spawn,
)
def _update_bots(self) -> None:
bots = self._bots.get_living_bots()
for bot in bots:
bot.target_flag = None
# If we're waiting on a continue, stop here so they don't keep scoring.
if self.is_waiting_for_continue():
self._bots.stop_moving()
return
# If we've got a flag and no player are holding it, find the closest
# bot to it, and make them the designated flag-bearer.
assert self._flag is not None
if self._flag.node:
for player in self.players:
if player.actor:
assert isinstance(player.actor, PlayerSpaz)
if (
player.actor.is_alive()
and player.actor.node.hold_node == self._flag.node
):
return
flagpos = ba.Vec3(self._flag.node.position)
closest_bot: SpazBot | None = None
closest_dist = 0.0 # Always gets assigned first time through.
for bot in bots:
# If a bot is picked up, he should forget about the flag.
if bot.held_count > 0:
continue
assert bot.node
botpos = ba.Vec3(bot.node.position)
botdist = (botpos - flagpos).length()
if closest_bot is None or botdist < closest_dist:
closest_bot = bot
closest_dist = botdist
if closest_bot is not None:
closest_bot.target_flag = self._flag
def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None:
if poweruptype is None:
poweruptype = PowerupBoxFactory.get().get_random_powerup_type(
excludetypes=self._exclude_powerups
)
PowerupBox(
position=self.map.powerup_spawn_points[index],
poweruptype=poweruptype,
).autoretain()
def _start_powerup_drops(self) -> None:
self._powerup_drop_timer = ba.Timer(
3.0, self._drop_powerups, repeat=True
)
def _drop_powerups(
self, standard_points: bool = False, poweruptype: str | None = None
) -> None:
"""Generic powerup drop."""
if standard_points:
spawnpoints = self.map.powerup_spawn_points
for i, _point in enumerate(spawnpoints):
ba.timer(
1.0 + i * 0.5, ba.Call(self._drop_powerup, i, poweruptype)
)
else:
point = (
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(
position=point,
poweruptype=PowerupBoxFactory.get().get_random_powerup_type(
excludetypes=self._exclude_powerups
),
).autoretain()
def _kill_flag(self) -> None:
try:
assert self._flag is not None
self._flag.handlemessage(ba.DieMessage())
except Exception:
ba.print_exception('Error in _kill_flag.')
def _handle_score(self) -> None:
"""a point has been scored"""
# FIXME tidy this up
# pylint: disable=too-many-branches
# Our flag might stick around for a second or two;
# we don't want it to be able to score again.
assert self._flag is not None
if self._flag.scored:
return
# See which score region it was.
region = ba.getcollision().sourcenode
i = None
for i, score_region in enumerate(self._score_regions):
if region == score_region.node:
break
for team in [self.teams[0], self._bot_team]:
assert team is not None
if team.id == i:
team.score += 7
# Tell all players (or bots) to celebrate.
if i == 0:
for player in team.players:
if player.actor:
player.actor.handlemessage(ba.CelebrateMessage(2.0))
else:
self._bots.celebrate(2.0)
# If the good guys scored, add more enemies.
if i == 0:
if self.teams[0].score == 7:
assert self._bot_types_7 is not None
for bottype in self._bot_types_7:
self._spawn_bot(bottype)
elif self.teams[0].score == 14:
assert self._bot_types_14 is not None
for bottype in self._bot_types_14:
self._spawn_bot(bottype)
ba.playsound(self._score_sound)
if i == 0:
ba.playsound(self._cheer_sound)
else:
ba.playsound(self._boo_sound)
# Kill the flag (it'll respawn shortly).
self._flag.scored = True
ba.timer(0.2, self._kill_flag)
self.update_scores()
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)
if i == 0:
ba.cameraflash(duration=10.0)
def end_game(self) -> None:
ba.setmusic(None)
self._bots.final_celebrate()
ba.timer(0.001, ba.Call(self.do_end, 'defeat'))
def on_continue(self) -> None:
# Subtract one touchdown from the bots and get them moving again.
assert self._bot_team is not None
self._bot_team.score -= 7
self._bots.start_moving()
self.update_scores()
def update_scores(self) -> None:
"""update scoreboard and check for winners"""
# FIXME: tidy this up
# pylint: disable=too-many-nested-blocks
have_scoring_team = False
win_score = self._score_to_win
for team in [self.teams[0], self._bot_team]:
assert team is not None
assert self._scoreboard is not None
self._scoreboard.set_team_value(team, team.score, win_score)
if team.score >= win_score:
if not have_scoring_team:
self._scoring_team = team
if team is self._bot_team:
self.continue_or_end_game()
else:
ba.setmusic(ba.MusicType.VICTORY)
# Completion achievements.
assert self._bot_team is not None
if self._preset in ['rookie', 'rookie_easy']:
self._award_achievement(
'Rookie Football Victory', sound=False
)
if self._bot_team.score == 0:
self._award_achievement(
'Rookie Football Shutout', sound=False
)
elif self._preset in ['pro', 'pro_easy']:
self._award_achievement(
'Pro Football Victory', sound=False
)
if self._bot_team.score == 0:
self._award_achievement(
'Pro Football Shutout', sound=False
)
elif self._preset in ['uber', 'uber_easy']:
self._award_achievement(
'Uber Football Victory', sound=False
)
if self._bot_team.score == 0:
self._award_achievement(
'Uber Football Shutout', sound=False
)
if (
not self._player_has_dropped_bomb
and not self._player_has_punched
):
self._award_achievement(
'Got the Moves', sound=False
)
self._bots.stop_moving()
self.show_zoom_message(
ba.Lstr(resource='victoryText'),
scale=1.0,
duration=4.0,
)
self.celebrate(10.0)
assert self._starttime_ms is not None
self._final_time_ms = int(
ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
- self._starttime_ms
)
self._time_text_timer = None
assert (
self._time_text_input is not None
and self._time_text_input.node
)
self._time_text_input.node.timemax = self._final_time_ms
# FIXME: Does this still need to be deferred?
ba.pushcall(ba.Call(self.do_end, 'victory'))
def do_end(self, outcome: str) -> None:
"""End the game with the specified outcome."""
if outcome == 'defeat':
self.fade_to_red()
assert self._final_time_ms is not None
scoreval = (
None if outcome == 'defeat' else int(self._final_time_ms // 10)
)
self.end(
delay=3.0,
results={
'outcome': outcome,
'score': scoreval,
'score_order': 'decreasing',
'playerinfos': self.initialplayerinfos,
},
)
def handlemessage(self, msg: Any) -> Any:
"""handle high-level game messages"""
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
# Respawn them shortly.
player = msg.getplayer(Player)
assert self.initialplayerinfos is not None
respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
player.respawn_timer = ba.Timer(
respawn_time, ba.Call(self.spawn_player_if_exists, player)
)
player.respawn_icon = RespawnIcon(player, respawn_time)
elif isinstance(msg, SpazBotDiedMessage):
# Every time a bad guy dies, spawn a new one.
ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot))))
elif isinstance(msg, SpazBotPunchedMessage):
if self._preset in ['rookie', 'rookie_easy']:
if msg.damage >= 500:
self._award_achievement('Super Punch')
elif self._preset in ['pro', 'pro_easy']:
if msg.damage >= 1000:
self._award_achievement('Super Mega Punch')
# Respawn dead flags.
elif isinstance(msg, FlagDiedMessage):
assert isinstance(msg.flag, FootballFlag)
msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag)
self._flag_respawn_light = ba.NodeActor(
ba.newnode(
'light',
attrs={
'position': self._flag_spawn_pos,
'height_attenuated': False,
'radius': 0.15,
'color': (1.0, 1.0, 0.3),
},
)
)
assert self._flag_respawn_light.node
ba.animate(
self._flag_respawn_light.node,
'intensity',
{0: 0, 0.25: 0.15, 0.5: 0},
loop=True,
)
ba.timer(3.0, self._flag_respawn_light.node.delete)
else:
return super().handlemessage(msg)
return None
def _handle_player_dropped_bomb(self, player: Spaz, bomb: ba.Actor) -> None:
del player, bomb # Unused.
self._player_has_dropped_bomb = True
def _handle_player_punched(self, player: Spaz) -> None:
del player # Unused.
self._player_has_punched = True
def spawn_player(self, player: Player) -> ba.Actor:
spaz = self.spawn_player_spaz(
player, position=self.map.get_start_position(player.team.id)
)
if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']:
spaz.impact_scale = 0.25
spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb)
spaz.punch_callback = self._handle_player_punched
return spaz
def _flash_flag_spawn(self) -> None:
light = ba.newnode(
'light',
attrs={
'position': self._flag_spawn_pos,
'height_attenuated': False,
'color': (1, 1, 0),
},
)
ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True)
ba.timer(1.0, light.delete)
def _spawn_flag(self) -> None:
ba.playsound(self._swipsound)
ba.playsound(self._whistle_sound)
self._flash_flag_spawn()
assert self._flag_spawn_pos is not None
self._flag = FootballFlag(position=self._flag_spawn_pos)