mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2026-08-15 13:04:50 +00:00
988 lines
35 KiB
Python
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)
|