bombsquad-plugin-manager/plugins/minigames/squid_race.py

954 lines
38 KiB
Python
Raw Normal View History

2024-01-17 23:09:18 +03:00
# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport)
2022-11-03 22:59:16 +05:30
# Released under the MIT License. See LICENSE for details.
#
"""Defines Race mini-game."""
2024-01-17 23:09:18 +03:00
# ba_meta require api 8
2022-11-03 22:59:16 +05:30
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import random
from typing import TYPE_CHECKING
from dataclasses import dataclass
2024-01-17 23:09:18 +03:00
import babase
import bauiv1 as bui
import bascenev1 as bs
from bascenev1lib.actor.bomb import Bomb, Blast, ExplodeHitMessage
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.gameutils import SharedObjects
2022-11-03 22:59:16 +05:30
if TYPE_CHECKING:
from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict,
Union)
2024-01-17 23:09:18 +03:00
from bascenev1lib.actor.onscreentimer import OnScreenTimer
2022-11-03 22:59:16 +05:30
class NewBlast(Blast):
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ExplodeHitMessage):
pass
else:
return super().handlemessage(msg)
2022-11-03 17:29:43 +00:00
2022-11-03 22:59:16 +05:30
@dataclass
class RaceMine:
"""Holds info about a mine on the track."""
point: Sequence[float]
mine: Optional[Bomb]
2024-01-17 23:09:18 +03:00
class RaceRegion(bs.Actor):
2022-11-03 22:59:16 +05:30
"""Region used to track progress during a race."""
def __init__(self, pt: Sequence[float], index: int):
super().__init__()
activity = self.activity
assert isinstance(activity, RaceGame)
self.pos = pt
self.index = index
2024-01-17 23:09:18 +03:00
self.node = bs.newnode(
2022-11-03 22:59:16 +05:30
'region',
delegate=self,
attrs={
'position': pt[:3],
'scale': (pt[3] * 2.0, pt[4] * 2.0, pt[5] * 2.0),
'type': 'box',
'materials': [activity.race_region_material]
})
2024-01-17 23:09:18 +03:00
class Player(bs.Player['Team']):
2022-11-03 22:59:16 +05:30
"""Our player type for this game."""
def __init__(self) -> None:
2024-01-17 23:09:18 +03:00
self.distance_txt: Optional[bs.Node] = None
2022-11-03 22:59:16 +05:30
self.last_region = 0
self.lap = 0
self.distance = 0.0
self.finished = False
self.rank: Optional[int] = None
2024-01-17 23:09:18 +03:00
class Team(bs.Team[Player]):
2022-11-03 22:59:16 +05:30
"""Our team type for this game."""
def __init__(self) -> None:
self.time: Optional[float] = None
self.lap = 0
self.finished = False
2024-01-17 23:09:18 +03:00
# ba_meta export bascenev1.GameActivity
class SquidRaceGame(bs.TeamGameActivity[Player, Team]):
2022-11-03 22:59:16 +05:30
"""Game of racing around a track."""
name = 'Squid Race'
description = 'Run real fast!'
2024-01-17 23:09:18 +03:00
scoreconfig = bs.ScoreConfig(label='Time',
2022-11-03 22:59:16 +05:30
lower_is_better=True,
2024-01-17 23:09:18 +03:00
scoretype=bs.ScoreType.MILLISECONDS)
2022-11-03 22:59:16 +05:30
@classmethod
def get_available_settings(
2024-01-17 23:09:18 +03:00
cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]:
2022-11-03 22:59:16 +05:30
settings = [
2024-01-17 23:09:18 +03:00
bs.IntSetting('Laps', min_value=1, default=3, increment=1),
bs.IntChoiceSetting(
2022-11-03 22:59:16 +05:30
'Time Limit',
default=0,
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
),
2024-01-17 23:09:18 +03:00
bs.IntChoiceSetting(
2022-11-03 22:59:16 +05:30
'Mine Spawning',
default=4000,
choices=[
('No Mines', 0),
('8 Seconds', 8000),
('4 Seconds', 4000),
('2 Seconds', 2000),
],
),
2024-01-17 23:09:18 +03:00
bs.IntChoiceSetting(
2022-11-03 22:59:16 +05:30
'Bomb Spawning',
choices=[
('None', 0),
('8 Seconds', 8000),
('4 Seconds', 4000),
('2 Seconds', 2000),
('1 Second', 1000),
],
default=2000,
),
2024-01-17 23:09:18 +03:00
bs.BoolSetting('Epic Mode', default=False),
2022-11-03 22:59:16 +05:30
]
# We have some specific settings in teams mode.
2024-01-17 23:09:18 +03:00
if issubclass(sessiontype, bs.DualTeamSession):
2022-11-03 22:59:16 +05:30
settings.append(
2024-01-17 23:09:18 +03:00
bs.BoolSetting('Entire Team Must Finish', default=False))
2022-11-03 22:59:16 +05:30
return settings
@classmethod
2024-01-17 23:09:18 +03:00
def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool:
return issubclass(sessiontype, bs.MultiTeamSession)
2022-11-03 22:59:16 +05:30
@classmethod
2024-01-17 23:09:18 +03:00
def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]:
return bs.app.classic.getmaps('race')
2022-11-03 22:59:16 +05:30
def __init__(self, settings: dict):
self._race_started = False
super().__init__(settings)
self._scoreboard = Scoreboard()
2024-01-17 23:09:18 +03:00
self._score_sound = bs.getsound('score')
self._swipsound = bs.getsound('swip')
2022-11-03 22:59:16 +05:30
self._last_team_time: Optional[float] = None
self._front_race_region: Optional[int] = None
2024-01-17 23:09:18 +03:00
self._nub_tex = bs.gettexture('nub')
self._beep_1_sound = bs.getsound('raceBeep1')
self._beep_2_sound = bs.getsound('raceBeep2')
self.race_region_material: Optional[bs.Material] = None
2022-11-03 22:59:16 +05:30
self._regions: List[RaceRegion] = []
self._team_finish_pts: Optional[int] = None
2024-01-17 23:09:18 +03:00
self._time_text: Optional[bs.Actor] = None
2022-11-03 22:59:16 +05:30
self._timer: Optional[OnScreenTimer] = None
self._race_mines: Optional[List[RaceMine]] = None
2024-01-17 23:09:18 +03:00
self._race_mine_timer: Optional[bs.Timer] = None
self._scoreboard_timer: Optional[bs.Timer] = None
self._player_order_update_timer: Optional[bs.Timer] = None
self._start_lights: Optional[List[bs.Node]] = None
self._squid_lights: Optional[List[bs.Node]] = None
2022-11-03 22:59:16 +05:30
self._countdown_timer: int = 0
self._sq_mode: str = 'Easy'
2024-01-17 23:09:18 +03:00
self._tick_timer: Optional[bs.Timer] = None
self._bomb_spawn_timer: Optional[bs.Timer] = None
2022-11-03 22:59:16 +05:30
self._laps = int(settings['Laps'])
self._entire_team_must_finish = bool(
settings.get('Entire Team Must Finish', False))
self._time_limit = float(settings['Time Limit'])
self._mine_spawning = int(settings['Mine Spawning'])
self._bomb_spawning = int(settings['Bomb Spawning'])
self._epic_mode = bool(settings['Epic Mode'])
self._countdownsounds = {
2024-01-17 23:09:18 +03:00
10: bs.getsound('announceTen'),
9: bs.getsound('announceNine'),
8: bs.getsound('announceEight'),
7: bs.getsound('announceSeven'),
6: bs.getsound('announceSix'),
5: bs.getsound('announceFive'),
4: bs.getsound('announceFour'),
3: bs.getsound('announceThree'),
2: bs.getsound('announceTwo'),
1: bs.getsound('announceOne')
2022-11-03 22:59:16 +05:30
}
# Base class overrides.
self.slow_motion = self._epic_mode
2024-01-17 23:09:18 +03:00
self.default_music = (bs.MusicType.EPIC_RACE
if self._epic_mode else bs.MusicType.RACE)
2022-11-03 22:59:16 +05:30
def get_instance_description(self) -> Union[str, Sequence]:
2024-01-17 23:09:18 +03:00
if (isinstance(self.session, bs.DualTeamSession)
2022-11-03 22:59:16 +05:30
and self._entire_team_must_finish):
t_str = ' Your entire team has to finish.'
else:
t_str = ''
if self._laps > 1:
return 'Run ${ARG1} laps.' + t_str, self._laps
return 'Run 1 lap.' + t_str
def get_instance_description_short(self) -> Union[str, Sequence]:
if self._laps > 1:
return 'run ${ARG1} laps', self._laps
return 'run 1 lap'
def on_transition_in(self) -> None:
super().on_transition_in()
shared = SharedObjects.get()
pts = self.map.get_def_points('race_point')
2024-01-17 23:09:18 +03:00
mat = self.race_region_material = bs.Material()
2022-11-03 22:59:16 +05:30
mat.add_actions(conditions=('they_have_material',
shared.player_material),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('call', 'at_connect',
self._handle_race_point_collide),
2022-11-03 17:29:43 +00:00
))
2022-11-03 22:59:16 +05:30
for rpt in pts:
self._regions.append(RaceRegion(rpt, len(self._regions)))
def _flash_player(self, player: Player, scale: float) -> None:
assert isinstance(player.actor, PlayerSpaz)
assert player.actor.node
pos = player.actor.node.position
2024-01-17 23:09:18 +03:00
light = bs.newnode('light',
2022-11-03 22:59:16 +05:30
attrs={
'position': pos,
'color': (1, 1, 0),
'height_attenuated': False,
'radius': 0.4
})
2024-01-17 23:09:18 +03:00
bs.timer(0.5, light.delete)
bs.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0})
2022-11-03 22:59:16 +05:30
def _handle_race_point_collide(self) -> None:
# FIXME: Tidy this up.
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-nested-blocks
2024-01-17 23:09:18 +03:00
collision = bs.getcollision()
2022-11-03 22:59:16 +05:30
try:
region = collision.sourcenode.getdelegate(RaceRegion, True)
player = collision.opposingnode.getdelegate(PlayerSpaz,
True).getplayer(
Player, True)
2024-01-17 23:09:18 +03:00
except bs.NotFoundError:
2022-11-03 22:59:16 +05:30
return
last_region = player.last_region
this_region = region.index
if last_region != this_region:
# If a player tries to skip regions, smite them.
# Allow a one region leeway though (its plausible players can get
# blown over a region, etc).
if this_region > last_region + 2:
if player.is_alive():
assert player.actor
2024-01-17 23:09:18 +03:00
player.actor.handlemessage(bs.DieMessage())
bs.broadcastmessage(babase.Lstr(
2022-11-03 22:59:16 +05:30
translate=('statements', 'Killing ${NAME} for'
' skipping part of the track!'),
subs=[('${NAME}', player.getname(full=True))]),
2022-11-03 17:29:43 +00:00
color=(1, 0, 0))
2022-11-03 22:59:16 +05:30
else:
# If this player is in first, note that this is the
# front-most race-point.
if player.rank == 0:
self._front_race_region = this_region
player.last_region = this_region
if last_region >= len(self._regions) - 2 and this_region == 0:
team = player.team
player.lap = min(self._laps, player.lap + 1)
# In teams mode with all-must-finish on, the team lap
# value is the min of all team players.
# Otherwise its the max.
2024-01-17 23:09:18 +03:00
if isinstance(self.session, bs.DualTeamSession
2022-11-03 22:59:16 +05:30
) and self._entire_team_must_finish:
team.lap = min([p.lap for p in team.players])
else:
team.lap = max([p.lap for p in team.players])
# A player is finishing.
if player.lap == self._laps:
# In teams mode, hand out points based on the order
# players come in.
2024-01-17 23:09:18 +03:00
if isinstance(self.session, bs.DualTeamSession):
2022-11-03 22:59:16 +05:30
assert self._team_finish_pts is not None
if self._team_finish_pts > 0:
self.stats.player_scored(player,
self._team_finish_pts,
screenmessage=False)
self._team_finish_pts -= 25
# Flash where the player is.
self._flash_player(player, 1.0)
player.finished = True
assert player.actor
player.actor.handlemessage(
2024-01-17 23:09:18 +03:00
bs.DieMessage(immediate=True))
2022-11-03 22:59:16 +05:30
# Makes sure noone behind them passes them in rank
# while finishing.
player.distance = 9999.0
# If the whole team has finished the race.
if team.lap == self._laps:
2024-01-17 23:09:18 +03:00
self._score_sound.play()
2022-11-03 22:59:16 +05:30
player.team.finished = True
assert self._timer is not None
2024-01-17 23:09:18 +03:00
elapsed = bs.time() - self._timer.getstarttime()
2022-11-03 22:59:16 +05:30
self._last_team_time = player.team.time = elapsed
# Team has yet to finish.
else:
2024-01-17 23:09:18 +03:00
self._swipsound.play()
2022-11-03 22:59:16 +05:30
# They've just finished a lap but not the race.
else:
2024-01-17 23:09:18 +03:00
self._swipsound.play()
2022-11-03 22:59:16 +05:30
self._flash_player(player, 0.3)
# Print their lap number over their head.
try:
assert isinstance(player.actor, PlayerSpaz)
2024-01-17 23:09:18 +03:00
mathnode = bs.newnode('math',
2022-11-03 22:59:16 +05:30
owner=player.actor.node,
attrs={
'input1': (0, 1.9, 0),
'operation': 'add'
})
player.actor.node.connectattr(
'torso_position', mathnode, 'input2')
2024-01-17 23:09:18 +03:00
tstr = babase.Lstr(resource='lapNumberText',
2022-11-03 22:59:16 +05:30
subs=[('${CURRENT}',
str(player.lap + 1)),
('${TOTAL}', str(self._laps))
])
2024-01-17 23:09:18 +03:00
txtnode = bs.newnode('text',
2022-11-03 22:59:16 +05:30
owner=mathnode,
attrs={
'text': tstr,
'in_world': True,
'color': (1, 1, 0, 1),
'scale': 0.015,
'h_align': 'center'
})
mathnode.connectattr('output', txtnode, 'position')
2024-01-17 23:09:18 +03:00
bs.animate(txtnode, 'scale', {
2022-11-03 22:59:16 +05:30
0.0: 0,
0.2: 0.019,
2.0: 0.019,
2.2: 0
})
2024-01-17 23:09:18 +03:00
bs.timer(2.3, mathnode.delete)
2022-11-03 22:59:16 +05:30
except Exception:
2024-01-17 23:09:18 +03:00
babase.print_exception('Error printing lap.')
2022-11-03 22:59:16 +05:30
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
def on_player_join(self, player: Player) -> None:
# Don't allow joining after we start
# (would enable leave/rejoin tomfoolery).
if self.has_begun():
2024-01-17 23:09:18 +03:00
bs.broadcastmessage(
babase.Lstr(resource='playerDelayedJoinText',
2022-11-03 22:59:16 +05:30
subs=[('${PLAYER}', player.getname(full=True))]),
color=(0, 1, 0),
)
return
self.spawn_player(player)
def on_player_leave(self, player: Player) -> None:
super().on_player_leave(player)
# A player leaving disqualifies the team if 'Entire Team Must Finish'
# is on (otherwise in teams mode everyone could just leave except the
# leading player to win).
2024-01-17 23:09:18 +03:00
if (isinstance(self.session, bs.DualTeamSession)
2022-11-03 22:59:16 +05:30
and self._entire_team_must_finish):
2024-01-17 23:09:18 +03:00
bs.broadcastmessage(babase.Lstr(
2022-11-03 22:59:16 +05:30
translate=('statements',
'${TEAM} is disqualified because ${PLAYER} left'),
subs=[('${TEAM}', player.team.name),
('${PLAYER}', player.getname(full=True))]),
2022-11-03 17:29:43 +00:00
color=(1, 1, 0))
2022-11-03 22:59:16 +05:30
player.team.finished = True
player.team.time = None
player.team.lap = 0
2024-01-17 23:09:18 +03:00
bs.getsound('boo').play()
2022-11-03 22:59:16 +05:30
for otherplayer in player.team.players:
otherplayer.lap = 0
otherplayer.finished = True
try:
if otherplayer.actor is not None:
2024-01-17 23:09:18 +03:00
otherplayer.actor.handlemessage(bs.DieMessage())
2022-11-03 22:59:16 +05:30
except Exception:
2024-01-17 23:09:18 +03:00
babase.print_exception('Error sending DieMessage.')
2022-11-03 22:59:16 +05:30
# Defer so team/player lists will be updated.
2024-01-17 23:09:18 +03:00
babase.pushcall(self._check_end_game)
2022-11-03 22:59:16 +05:30
def _update_scoreboard(self) -> None:
for team in self.teams:
distances = [player.distance for player in team.players]
if not distances:
teams_dist = 0.0
else:
2024-01-17 23:09:18 +03:00
if (isinstance(self.session, bs.DualTeamSession)
2022-11-03 22:59:16 +05:30
and self._entire_team_must_finish):
teams_dist = min(distances)
else:
teams_dist = max(distances)
self._scoreboard.set_team_value(
team,
teams_dist,
self._laps,
flash=(teams_dist >= float(self._laps)),
show_value=False)
def on_begin(self) -> None:
2024-01-17 23:09:18 +03:00
from bascenev1lib.actor.onscreentimer import OnScreenTimer
2022-11-03 22:59:16 +05:30
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
self._team_finish_pts = 100
# Throw a timer up on-screen.
2024-01-17 23:09:18 +03:00
self._time_text = bs.NodeActor(
bs.newnode('text',
2022-11-03 22:59:16 +05:30
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.4,
'text': ''
}))
self._timer = OnScreenTimer()
if self._mine_spawning != 0:
self._race_mines = [
RaceMine(point=p, mine=None)
for p in self.map.get_def_points('race_mine')
]
if self._race_mines:
2024-01-17 23:09:18 +03:00
self._race_mine_timer = bs.Timer(0.001 * self._mine_spawning,
2022-11-03 22:59:16 +05:30
self._update_race_mine,
repeat=True)
2024-01-17 23:09:18 +03:00
self._scoreboard_timer = bs.Timer(0.25,
2022-11-03 22:59:16 +05:30
self._update_scoreboard,
repeat=True)
2024-01-17 23:09:18 +03:00
self._player_order_update_timer = bs.Timer(0.25,
2022-11-03 22:59:16 +05:30
self._update_player_order,
repeat=True)
if self.slow_motion:
t_scale = 0.4
light_y = 50
else:
t_scale = 1.0
light_y = 150
lstart = 7.1 * t_scale
inc = 1.25 * t_scale
2024-01-17 23:09:18 +03:00
bs.timer(lstart, self._do_light_1)
bs.timer(lstart + inc, self._do_light_2)
bs.timer(lstart + 2 * inc, self._do_light_3)
bs.timer(lstart + 3 * inc, self._start_race)
2022-11-03 22:59:16 +05:30
self._start_lights = []
for i in range(4):
2024-01-17 23:09:18 +03:00
lnub = bs.newnode('image',
2022-11-03 22:59:16 +05:30
attrs={
2024-01-17 23:09:18 +03:00
'texture': bs.gettexture('nub'),
2022-11-03 22:59:16 +05:30
'opacity': 1.0,
'absolute_scale': True,
'position': (-75 + i * 50, light_y),
'scale': (50, 50),
'attach': 'center'
})
2024-01-17 23:09:18 +03:00
bs.animate(
2022-11-03 22:59:16 +05:30
lnub, 'opacity', {
4.0 * t_scale: 0,
5.0 * t_scale: 1.0,
12.0 * t_scale: 1.0,
12.5 * t_scale: 0.0
})
2024-01-17 23:09:18 +03:00
bs.timer(13.0 * t_scale, lnub.delete)
2022-11-03 22:59:16 +05:30
self._start_lights.append(lnub)
self._start_lights[0].color = (0.2, 0, 0)
self._start_lights[1].color = (0.2, 0, 0)
self._start_lights[2].color = (0.2, 0.05, 0)
self._start_lights[3].color = (0.0, 0.3, 0)
self._squid_lights = []
for i in range(2):
2024-01-17 23:09:18 +03:00
lnub = bs.newnode('image',
2022-11-03 22:59:16 +05:30
attrs={
2024-01-17 23:09:18 +03:00
'texture': bs.gettexture('nub'),
2022-11-03 22:59:16 +05:30
'opacity': 1.0,
'absolute_scale': True,
'position': (-33 + i * 65, 220),
'scale': (60, 60),
'attach': 'center'
})
2024-01-17 23:09:18 +03:00
bs.animate(
2022-11-03 22:59:16 +05:30
lnub, 'opacity', {
4.0 * t_scale: 0,
5.0 * t_scale: 1.0})
self._squid_lights.append(lnub)
self._squid_lights[0].color = (0.2, 0, 0)
self._squid_lights[1].color = (0.0, 0.3, 0)
2024-01-17 23:09:18 +03:00
bs.timer(1.0, self._check_squid_end, repeat=True)
2022-11-03 22:59:16 +05:30
self._squidgame_countdown()
def _squidgame_countdown(self) -> None:
2022-11-03 17:29:43 +00:00
self._countdown_timer = 80 * self._laps # 80
2024-01-17 23:09:18 +03:00
bs.newnode(
2022-11-03 22:59:16 +05:30
'image',
attrs={
'opacity': 0.7,
'color': (0.2, 0.2, 0.2),
'attach': 'topCenter',
'position': (-220, -40),
'scale': (135, 45),
2024-01-17 23:09:18 +03:00
'texture': bs.gettexture('bar')})
bs.newnode(
2022-11-03 22:59:16 +05:30
'image',
attrs={
'opacity': 1.0,
'color': (1.0, 0.0, 0.0),
'attach': 'topCenter',
'position': (-220, -38),
2022-11-03 17:29:43 +00:00
'scale': (155, 65),
2024-01-17 23:09:18 +03:00
'texture': bs.gettexture('uiAtlas'),
'mesh_transparent': bs.getmesh('meterTransparent')})
self._sgcountdown_text = bs.newnode(
2022-11-03 22:59:16 +05:30
'text',
attrs={
'v_attach': 'top',
'h_attach': 'center',
'h_align': 'center',
'position': (-220, -57),
'shadow': 0.5,
'flatness': 0.5,
'scale': 1.1,
'text': str(self._countdown_timer)+"s"})
def _update_sgcountdown(self) -> None:
self._countdown_timer -= 1
self._countdown_timer
if self._countdown_timer <= 0:
self._countdown_timer = 0
self._squid_game_all_die()
if self._countdown_timer == 20:
self._sq_mode = 'Hard'
2024-01-17 23:09:18 +03:00
bs.getsound('alarm').play()
2022-11-03 22:59:16 +05:30
if self._countdown_timer == 40:
self._sq_mode = 'Normal'
if self._countdown_timer <= 20:
self._sgcountdown_text.color = (1.2, 0.0, 0.0)
self._sgcountdown_text.scale = 1.2
if self._countdown_timer in self._countdownsounds:
2024-01-17 23:09:18 +03:00
self._countdownsounds[self._countdown_timer].play()
2022-11-03 22:59:16 +05:30
else:
self._sgcountdown_text.color = (1.0, 1.0, 1.0)
self._sgcountdown_text.text = str(self._countdown_timer)+"s"
def _squid_game_all_die(self) -> None:
for player in self.players:
if player.is_alive():
player.actor._cursed = True
2024-01-17 23:09:18 +03:00
player.actor.handlemessage(bs.DieMessage())
2022-11-03 22:59:16 +05:30
NewBlast(
position=player.actor.node.position,
velocity=player.actor.node.velocity,
blast_radius=3.0,
blast_type='normal').autoretain()
player.actor.handlemessage(
2024-01-17 23:09:18 +03:00
bs.HitMessage(
2022-11-03 22:59:16 +05:30
pos=player.actor.node.position,
velocity=player.actor.node.velocity,
magnitude=2000,
hit_type='explosion',
hit_subtype='normal',
radius=2.0,
source_player=None))
player.actor._cursed = False
def _do_ticks(self) -> None:
def do_ticks():
if self._ticks:
2024-01-17 23:09:18 +03:00
bs.getsound('tick').play()
self._tick_timer = bs.timer(1.0, do_ticks, repeat=True)
2022-11-03 22:59:16 +05:30
def _start_squid_game(self) -> None:
2022-11-03 17:29:43 +00:00
easy = [4.5, 5, 5.5, 6]
normal = [4, 4.5, 5]
hard = [3, 3.5, 4]
2022-11-03 22:59:16 +05:30
random_number = random.choice(
hard if self._sq_mode == 'Hard' else
normal if self._sq_mode == 'Normal' else easy)
# if random_number == 6:
2024-01-17 23:09:18 +03:00
# bs.getsound('lrlg_06s').play()
2022-11-03 22:59:16 +05:30
# elif random_number == 5.5:
2024-01-17 23:09:18 +03:00
# bs.getsound('lrlg_055s').play()
2022-11-03 22:59:16 +05:30
# elif random_number == 5:
2024-01-17 23:09:18 +03:00
# bs.getsound('lrlg_05s').play()
2022-11-03 22:59:16 +05:30
# elif random_number == 4.5:
2024-01-17 23:09:18 +03:00
# bs.getsound('lrlg_045s').play()
2022-11-03 22:59:16 +05:30
# elif random_number == 4:
2024-01-17 23:09:18 +03:00
# bs.getsound('lrlg_04s').play()
2022-11-03 22:59:16 +05:30
# elif random_number == 3.5:
2024-01-17 23:09:18 +03:00
# bs.getsound('lrlg_035s').play()
2022-11-03 22:59:16 +05:30
# elif random_number == 3:
2024-01-17 23:09:18 +03:00
# bs.getsound('lrlg_03s').play()
2022-11-03 22:59:16 +05:30
self._squid_lights[0].color = (0.2, 0, 0)
self._squid_lights[1].color = (0.0, 1.0, 0)
self._do_delete = False
self._ticks = True
2024-01-17 23:09:18 +03:00
bs.timer(random_number, self._stop_squid_game)
2022-11-03 22:59:16 +05:30
def _stop_squid_game(self) -> None:
self._ticks = False
self._squid_lights[0].color = (1.0, 0, 0)
self._squid_lights[1].color = (0.0, 0.3, 0)
2024-01-17 23:09:18 +03:00
bs.timer(0.2, self._check_delete)
2022-11-03 22:59:16 +05:30
def _check_delete(self) -> None:
for player in self.players:
if player.is_alive():
player.customdata['position'] = None
player.customdata['position'] = player.actor.node.position
self._do_delete = True
2024-01-17 23:09:18 +03:00
bs.timer(3.0 if self._sq_mode == 'Hard' else 4.0,
2022-11-03 22:59:16 +05:30
self._start_squid_game)
def _start_delete(self) -> None:
for player in self.players:
if player.is_alive() and self._do_delete:
posx = float("%.1f" % player.customdata['position'][0])
posz = float("%.1f" % player.customdata['position'][1])
posy = float("%.1f" % player.customdata['position'][2])
posx_list = [
2022-11-03 17:29:43 +00:00
round(posx, 1), round(posx+0.1, 1), round(posx+0.2, 1),
round(posx-0.1, 1), round(posx-0.2, 1)]
2022-11-03 22:59:16 +05:30
current_posx = float("%.1f" % player.actor.node.position[0])
posz_list = [
2022-11-03 17:29:43 +00:00
round(posz, 1), round(posz+0.1, 1), round(posz+0.2, 1),
round(posz-0.1, 1), round(posz-0.2, 1)]
2022-11-03 22:59:16 +05:30
current_posz = float("%.1f" % player.actor.node.position[1])
posy_list = [
2022-11-03 17:29:43 +00:00
round(posy, 1), round(posy+0.1, 1), round(posy+0.2, 1),
round(posy-0.1, 1), round(posy-0.2, 1)]
2022-11-03 22:59:16 +05:30
current_posy = float("%.1f" % player.actor.node.position[2])
if not (current_posx in posx_list) or not (
current_posz in posz_list) or not (
current_posy in posy_list):
player.actor._cursed = True
2024-01-17 23:09:18 +03:00
player.actor.handlemessage(bs.DieMessage())
2022-11-03 22:59:16 +05:30
NewBlast(
position=player.actor.node.position,
velocity=player.actor.node.velocity,
blast_radius=3.0,
blast_type='normal').autoretain()
player.actor.handlemessage(
2024-01-17 23:09:18 +03:00
bs.HitMessage(
2022-11-03 22:59:16 +05:30
pos=player.actor.node.position,
velocity=player.actor.node.velocity,
magnitude=2000,
hit_type='explosion',
hit_subtype='normal',
radius=2.0,
source_player=None))
player.actor._cursed = False
def _check_squid_end(self) -> None:
squid_player_alive = 0
for player in self.players:
if player.is_alive():
squid_player_alive += 1
break
if squid_player_alive < 1:
self.end_game()
def _do_light_1(self) -> None:
assert self._start_lights is not None
self._start_lights[0].color = (1.0, 0, 0)
2024-01-17 23:09:18 +03:00
self._beep_1_sound.play()
2022-11-03 22:59:16 +05:30
def _do_light_2(self) -> None:
assert self._start_lights is not None
self._start_lights[1].color = (1.0, 0, 0)
2024-01-17 23:09:18 +03:00
self._beep_1_sound.play()
2022-11-03 22:59:16 +05:30
def _do_light_3(self) -> None:
assert self._start_lights is not None
self._start_lights[2].color = (1.0, 0.3, 0)
2024-01-17 23:09:18 +03:00
self._beep_1_sound.play()
2022-11-03 22:59:16 +05:30
def _start_race(self) -> None:
assert self._start_lights is not None
self._start_lights[3].color = (0.0, 1.0, 0)
2024-01-17 23:09:18 +03:00
self._beep_2_sound.play()
2022-11-03 22:59:16 +05:30
for player in self.players:
if player.actor is not None:
try:
assert isinstance(player.actor, PlayerSpaz)
player.actor.connect_controls_to_player()
except Exception:
2024-01-17 23:09:18 +03:00
babase.print_exception('Error in race player connects.')
2022-11-03 22:59:16 +05:30
assert self._timer is not None
self._timer.start()
if self._bomb_spawning != 0:
2024-01-17 23:09:18 +03:00
self._bomb_spawn_timer = bs.Timer(0.001 * self._bomb_spawning,
2022-11-03 22:59:16 +05:30
self._spawn_bomb,
repeat=True)
self._race_started = True
self._squid_lights[1].color = (0.0, 1.0, 0)
self._start_squid_game()
self._do_ticks()
2024-01-17 23:09:18 +03:00
bs.timer(0.2, self._start_delete, repeat=True)
bs.timer(1.0, self._update_sgcountdown, repeat=True)
2022-11-03 22:59:16 +05:30
def _update_player_order(self) -> None:
# Calc all player distances.
for player in self.players:
2024-01-17 23:09:18 +03:00
pos: Optional[babase.Vec3]
2022-11-03 22:59:16 +05:30
try:
pos = player.position
2024-01-17 23:09:18 +03:00
except bs.NotFoundError:
2022-11-03 22:59:16 +05:30
pos = None
if pos is not None:
r_index = player.last_region
rg1 = self._regions[r_index]
2024-01-17 23:09:18 +03:00
r1pt = babase.Vec3(rg1.pos[:3])
2022-11-03 22:59:16 +05:30
rg2 = self._regions[0] if r_index == len(
self._regions) - 1 else self._regions[r_index + 1]
2024-01-17 23:09:18 +03:00
r2pt = babase.Vec3(rg2.pos[:3])
2022-11-03 22:59:16 +05:30
r2dist = (pos - r2pt).length()
amt = 1.0 - (r2dist / (r2pt - r1pt).length())
amt = player.lap + (r_index + amt) * (1.0 / len(self._regions))
player.distance = amt
# Sort players by distance and update their ranks.
p_list = [(player.distance, player) for player in self.players]
p_list.sort(reverse=True, key=lambda x: x[0])
for i, plr in enumerate(p_list):
plr[1].rank = i
if plr[1].actor:
node = plr[1].distance_txt
if node:
node.text = str(i + 1) if plr[1].is_alive() else ''
def _spawn_bomb(self) -> None:
if self._front_race_region is None:
return
region = (self._front_race_region + 3) % len(self._regions)
pos = self._regions[region].pos
# Don't use the full region so we're less likely to spawn off a cliff.
region_scale = 0.8
x_range = ((-0.5, 0.5) if pos[3] == 0 else
(-region_scale * pos[3], region_scale * pos[3]))
z_range = ((-0.5, 0.5) if pos[5] == 0 else
(-region_scale * pos[5], region_scale * pos[5]))
pos = (pos[0] + random.uniform(*x_range), pos[1] + 1.0,
pos[2] + random.uniform(*z_range))
2024-01-17 23:09:18 +03:00
bs.timer(random.uniform(0.0, 2.0),
bs.WeakCall(self._spawn_bomb_at_pos, pos))
2022-11-03 22:59:16 +05:30
def _spawn_bomb_at_pos(self, pos: Sequence[float]) -> None:
if self.has_ended():
return
Bomb(position=pos, bomb_type='normal').autoretain()
def _make_mine(self, i: int) -> None:
assert self._race_mines is not None
rmine = self._race_mines[i]
rmine.mine = Bomb(position=rmine.point[:3], bomb_type='land_mine')
rmine.mine.arm()
def _flash_mine(self, i: int) -> None:
assert self._race_mines is not None
rmine = self._race_mines[i]
2024-01-17 23:09:18 +03:00
light = bs.newnode('light',
2022-11-03 22:59:16 +05:30
attrs={
'position': rmine.point[:3],
'color': (1, 0.2, 0.2),
'radius': 0.1,
'height_attenuated': False
})
2024-01-17 23:09:18 +03:00
bs.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True)
bs.timer(1.0, light.delete)
2022-11-03 22:59:16 +05:30
def _update_race_mine(self) -> None:
assert self._race_mines is not None
m_index = -1
rmine = None
for _i in range(3):
m_index = random.randrange(len(self._race_mines))
rmine = self._race_mines[m_index]
if not rmine.mine:
break
assert rmine is not None
if not rmine.mine:
self._flash_mine(m_index)
2024-01-17 23:09:18 +03:00
bs.timer(0.95, babase.Call(self._make_mine, m_index))
2022-11-03 22:59:16 +05:30
2024-01-17 23:09:18 +03:00
def spawn_player(self, player: Player) -> bs.Actor:
2022-11-03 22:59:16 +05:30
if player.team.finished:
# FIXME: This is not type-safe!
# This call is expected to always return an Actor!
# Perhaps we need something like can_spawn_player()...
# noinspection PyTypeChecker
return None # type: ignore
pos = self._regions[player.last_region].pos
# Don't use the full region so we're less likely to spawn off a cliff.
region_scale = 0.8
x_range = ((-0.5, 0.5) if pos[3] == 0 else
(-region_scale * pos[3], region_scale * pos[3]))
z_range = ((-0.5, 0.5) if pos[5] == 0 else
(-region_scale * pos[5], region_scale * pos[5]))
pos = (pos[0] + random.uniform(*x_range), pos[1],
pos[2] + random.uniform(*z_range))
spaz = self.spawn_player_spaz(
player, position=pos, angle=90 if not self._race_started else None)
assert spaz.node
# Prevent controlling of characters before the start of the race.
if not self._race_started:
spaz.disconnect_controls_from_player()
2024-01-17 23:09:18 +03:00
mathnode = bs.newnode('math',
2022-11-03 22:59:16 +05:30
owner=spaz.node,
attrs={
'input1': (0, 1.4, 0),
'operation': 'add'
})
spaz.node.connectattr('torso_position', mathnode, 'input2')
2024-01-17 23:09:18 +03:00
distance_txt = bs.newnode('text',
2022-11-03 22:59:16 +05:30
owner=spaz.node,
attrs={
'text': '',
'in_world': True,
'color': (1, 1, 0.4),
'scale': 0.02,
'h_align': 'center'
})
player.distance_txt = distance_txt
mathnode.connectattr('output', distance_txt, 'position')
return spaz
def _check_end_game(self) -> None:
# If there's no teams left racing, finish.
teams_still_in = len([t for t in self.teams if not t.finished])
if teams_still_in == 0:
self.end_game()
return
# Count the number of teams that have completed the race.
teams_completed = len(
[t for t in self.teams if t.finished and t.time is not None])
if teams_completed > 0:
session = self.session
# In teams mode its over as soon as any team finishes the race
# FIXME: The get_ffa_point_awards code looks dangerous.
2024-01-17 23:09:18 +03:00
if isinstance(session, bs.DualTeamSession):
2022-11-03 22:59:16 +05:30
self.end_game()
else:
# In ffa we keep the race going while there's still any points
# to be handed out. Find out how many points we have to award
# and how many teams have finished, and once that matches
# we're done.
2024-01-17 23:09:18 +03:00
assert isinstance(session, bs.FreeForAllSession)
2022-11-03 22:59:16 +05:30
points_to_award = len(session.get_ffa_point_awards())
if teams_completed >= points_to_award - teams_completed:
self.end_game()
return
def end_game(self) -> None:
# Stop updating our time text, and set it to show the exact last
# finish time if we have one. (so users don't get upset if their
# final time differs from what they see onscreen by a tiny amount)
assert self._timer is not None
if self._timer.has_started():
self._timer.stop(
endtime=None if self._last_team_time is None else (
self._timer.getstarttime() + self._last_team_time))
2024-01-17 23:09:18 +03:00
results = bs.GameResults()
2022-11-03 22:59:16 +05:30
for team in self.teams:
if team.time is not None:
# We store time in seconds, but pass a score in milliseconds.
results.set_team_score(team, int(team.time * 1000.0))
else:
results.set_team_score(team, None)
# We don't announce a winner in ffa mode since its probably been a
# while since the first place guy crossed the finish line so it seems
# odd to be announcing that now.
self.end(results=results,
announce_winning_team=isinstance(self.session,
2024-01-17 23:09:18 +03:00
bs.DualTeamSession))
2022-11-03 22:59:16 +05:30
def handlemessage(self, msg: Any) -> Any:
2024-01-17 23:09:18 +03:00
if isinstance(msg, bs.PlayerDiedMessage):
2022-11-03 22:59:16 +05:30
# Augment default behavior.
super().handlemessage(msg)
else:
super().handlemessage(msg)