mirror of
https://github.com/bombsquad-community/plugin-manager.git
synced 2025-10-08 14:54:36 +00:00
1239 lines
48 KiB
Python
1239 lines
48 KiB
Python
# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport)
|
|
# ba_meta require api 8
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
from typing import TYPE_CHECKING
|
|
from dataclasses import dataclass
|
|
|
|
import babase
|
|
import bauiv1 as bui
|
|
import bascenev1 as bs
|
|
from bascenev1 import _map
|
|
from bascenev1lib.actor.bomb import Bomb, Blast, BombFactory
|
|
from bascenev1lib.actor.powerupbox import PowerupBox
|
|
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
|
from bascenev1lib.actor.scoreboard import Scoreboard
|
|
from bascenev1lib.gameutils import SharedObjects
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict,
|
|
Union)
|
|
from bascenev1lib.actor.onscreentimer import OnScreenTimer
|
|
|
|
|
|
class ThePadDefs:
|
|
points = {}
|
|
boxes = {}
|
|
points['race_mine1'] = (0, 5, 12)
|
|
points['race_point1'] = (0.2, 5, 2.86308) + (0.507, 4.673, 1.1)
|
|
points['race_point2'] = (6.9301, 5.04988, 2.82066) + (0.911, 4.577, 1.073)
|
|
points['race_point3'] = (6.98857, 4.5011, -8.88703) + (1.083, 4.673, 1.076)
|
|
points['race_point4'] = (-6.4441, 4.5011, -8.88703) + (1.083, 4.673, 1.076)
|
|
points['race_point5'] = (-6.31128, 4.5011, 2.82669) + (0.894, 4.673, 0.941)
|
|
boxes['area_of_interest_bounds'] = (
|
|
0.3544110667, 4.493562578, -2.518391331) + (
|
|
0.0, 0.0, 0.0) + (16.64754831, 8.06138989, 18.5029888)
|
|
points['ffa_spawn1'] = (-0, 5, 2.5)
|
|
points['flag1'] = (-7.026110145, 4.308759233, -6.302807727)
|
|
points['flag2'] = (7.632557137, 4.366002373, -6.287969342)
|
|
points['flagDefault'] = (0.4611826686, 4.382076338, 3.680881802)
|
|
boxes['map_bounds'] = (0.2608783669, 4.899663734, -3.543675157) + (
|
|
0.0, 0.0, 0.0) + (29.23565494, 14.19991443, 29.92689344)
|
|
points['powerup_spawn1'] = (-4.166594349, 5.281834349, -6.427493781)
|
|
points['powerup_spawn2'] = (4.426873526, 5.342460464, -6.329745237)
|
|
points['powerup_spawn3'] = (-4.201686731, 5.123385835, 0.4400721376)
|
|
points['powerup_spawn4'] = (4.758924722, 5.123385835, 0.3494054559)
|
|
points['shadow_lower_bottom'] = (-0.2912522507, 2.020798381, 5.341226521)
|
|
points['shadow_lower_top'] = (-0.2912522507, 3.206066063, 5.341226521)
|
|
points['shadow_upper_bottom'] = (-0.2912522507, 6.062361813, 5.341226521)
|
|
points['shadow_upper_top'] = (-0.2912522507, 9.827201965, 5.341226521)
|
|
points['spawn1'] = (-0, 5, 2.5)
|
|
points['tnt1'] = (0.4599593402, 4.044276501, -6.573537395)
|
|
|
|
|
|
class ThePadMapb(bs.Map):
|
|
defs = ThePadDefs()
|
|
name = 'Racing'
|
|
|
|
@classmethod
|
|
def get_play_types(cls) -> List[str]:
|
|
"""Return valid play types for this map."""
|
|
return ['hyper']
|
|
|
|
@classmethod
|
|
def get_preview_texture_name(cls) -> str:
|
|
return 'thePadPreview'
|
|
|
|
@classmethod
|
|
def on_preload(cls) -> Any:
|
|
data: Dict[str, Any] = {
|
|
'mesh': bs.getmesh('thePadLevel'),
|
|
'bottom_mesh': bs.getmesh('thePadLevelBottom'),
|
|
'collision_mesh': bs.getcollisionmesh('thePadLevelCollide'),
|
|
'tex': bs.gettexture('thePadLevelColor'),
|
|
'bgtex': bs.gettexture('black'),
|
|
'bgmesh': bs.getmesh('thePadBG'),
|
|
'railing_collision_mesh': bs.getcollisionmesh('thePadLevelBumper'),
|
|
'vr_fill_mound_mesh': bs.getmesh('thePadVRFillMound'),
|
|
'vr_fill_mound_tex': bs.gettexture('vrFillMound')
|
|
}
|
|
# fixme should chop this into vr/non-vr sections for efficiency
|
|
return data
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
shared = SharedObjects.get()
|
|
self.node = bs.newnode(
|
|
'terrain',
|
|
delegate=self,
|
|
attrs={
|
|
'collision_mesh': self.preloaddata['collision_mesh'],
|
|
'mesh': self.preloaddata['mesh'],
|
|
'color_texture': self.preloaddata['tex'],
|
|
'materials': [shared.footing_material]
|
|
})
|
|
self.bottom = bs.newnode('terrain',
|
|
attrs={
|
|
'mesh': self.preloaddata['bottom_mesh'],
|
|
'lighting': False,
|
|
'color_texture': self.preloaddata['tex']
|
|
})
|
|
self.background = bs.newnode(
|
|
'terrain',
|
|
attrs={
|
|
'mesh': self.preloaddata['bgmesh'],
|
|
'lighting': False,
|
|
'background': True,
|
|
'color_texture': self.preloaddata['bgtex']
|
|
})
|
|
self.railing = bs.newnode(
|
|
'terrain',
|
|
attrs={
|
|
'collision_mesh': self.preloaddata['railing_collision_mesh'],
|
|
'materials': [shared.railing_material],
|
|
'bumper': True
|
|
})
|
|
bs.newnode('terrain',
|
|
attrs={
|
|
'mesh': self.preloaddata['vr_fill_mound_mesh'],
|
|
'lighting': False,
|
|
'vr_only': True,
|
|
'color': (0.56, 0.55, 0.47),
|
|
'background': True,
|
|
'color_texture': self.preloaddata['vr_fill_mound_tex']
|
|
})
|
|
gnode = bs.getactivity().globalsnode
|
|
gnode.tint = (1.1, 1.1, 1.0)
|
|
gnode.ambient_color = (1.1, 1.1, 1.0)
|
|
gnode.vignette_outer = (0.7, 0.65, 0.75)
|
|
gnode.vignette_inner = (0.95, 0.95, 0.93)
|
|
|
|
|
|
# ba_meta export plugin
|
|
class NewMap(babase.Plugin):
|
|
"""My first ballistica plugin!"""
|
|
|
|
def on_app_running(self) -> None:
|
|
_map.register_map(ThePadMapb)
|
|
|
|
|
|
class NewBlast(Blast):
|
|
|
|
def __init__(self,
|
|
position: Sequence[float] = (0.0, 1.0, 0.0),
|
|
velocity: Sequence[float] = (0.0, 0.0, 0.0),
|
|
blast_radius: float = 2.0,
|
|
blast_type: str = 'normal',
|
|
source_player: bs.Player = None,
|
|
hit_type: str = 'explosion',
|
|
hit_subtype: str = 'normal'):
|
|
bs.Actor.__init__(self)
|
|
|
|
shared = SharedObjects.get()
|
|
factory = BombFactory.get()
|
|
|
|
self.blast_type = blast_type
|
|
self._source_player = source_player
|
|
self.hit_type = hit_type
|
|
self.hit_subtype = hit_subtype
|
|
self.radius = blast_radius
|
|
|
|
# Set our position a bit lower so we throw more things upward.
|
|
rmats = (factory.blast_material, shared.attack_material)
|
|
self.node = bs.newnode(
|
|
'region',
|
|
delegate=self,
|
|
attrs={
|
|
'position': (position[0], position[1] - 0.1, position[2]),
|
|
'scale': (self.radius, self.radius, self.radius),
|
|
'type': 'sphere',
|
|
'materials': rmats
|
|
},
|
|
)
|
|
|
|
bs.timer(0.05, self.node.delete)
|
|
|
|
# Throw in an explosion and flash.
|
|
evel = (velocity[0], max(-1.0, velocity[1]), velocity[2])
|
|
explosion = bs.newnode('explosion',
|
|
attrs={
|
|
'position': position,
|
|
'velocity': evel,
|
|
'radius': self.radius,
|
|
'big': (self.blast_type == 'tnt')
|
|
})
|
|
if self.blast_type == 'ice':
|
|
explosion.color = (0, 0.05, 0.4)
|
|
|
|
bs.timer(1.0, explosion.delete)
|
|
|
|
if self.blast_type != 'ice':
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(1.0 + random.random() * 4),
|
|
emit_type='tendrils',
|
|
tendril_type='thin_smoke')
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(4.0 + random.random() * 4),
|
|
emit_type='tendrils',
|
|
tendril_type='ice' if self.blast_type == 'ice' else 'smoke')
|
|
bs.emitfx(position=position,
|
|
emit_type='distortion',
|
|
spread=1.0 if self.blast_type == 'tnt' else 2.0)
|
|
|
|
# And emit some shrapnel.
|
|
if self.blast_type == 'ice':
|
|
|
|
def emit() -> None:
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=30,
|
|
spread=2.0,
|
|
scale=0.4,
|
|
chunk_type='ice',
|
|
emit_type='stickers')
|
|
|
|
# It looks better if we delay a bit.
|
|
bs.timer(0.05, emit)
|
|
|
|
elif self.blast_type == 'sticky':
|
|
|
|
def emit() -> None:
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(4.0 + random.random() * 8),
|
|
spread=0.7,
|
|
chunk_type='slime')
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(4.0 + random.random() * 8),
|
|
scale=0.5,
|
|
spread=0.7,
|
|
chunk_type='slime')
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=15,
|
|
scale=0.6,
|
|
chunk_type='slime',
|
|
emit_type='stickers')
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=20,
|
|
scale=0.7,
|
|
chunk_type='spark',
|
|
emit_type='stickers')
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(6.0 + random.random() * 12),
|
|
scale=0.8,
|
|
spread=1.5,
|
|
chunk_type='spark')
|
|
|
|
# It looks better if we delay a bit.
|
|
bs.timer(0.05, emit)
|
|
|
|
elif self.blast_type == 'impact':
|
|
|
|
def emit() -> None:
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(4.0 + random.random() * 8),
|
|
scale=0.8,
|
|
chunk_type='metal')
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(4.0 + random.random() * 8),
|
|
scale=0.4,
|
|
chunk_type='metal')
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=20,
|
|
scale=0.7,
|
|
chunk_type='spark',
|
|
emit_type='stickers')
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(8.0 + random.random() * 15),
|
|
scale=0.8,
|
|
spread=1.5,
|
|
chunk_type='spark')
|
|
|
|
# It looks better if we delay a bit.
|
|
bs.timer(0.05, emit)
|
|
|
|
else: # Regular or land mine bomb shrapnel.
|
|
|
|
def emit() -> None:
|
|
if self.blast_type != 'tnt':
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(4.0 + random.random() * 8),
|
|
chunk_type='rock')
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(4.0 + random.random() * 8),
|
|
scale=0.5,
|
|
chunk_type='rock')
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=30,
|
|
scale=1.0 if self.blast_type == 'tnt' else 0.7,
|
|
chunk_type='spark',
|
|
emit_type='stickers')
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(18.0 + random.random() * 20),
|
|
scale=1.0 if self.blast_type == 'tnt' else 0.8,
|
|
spread=1.5,
|
|
chunk_type='spark')
|
|
|
|
# TNT throws splintery chunks.
|
|
if self.blast_type == 'tnt':
|
|
|
|
def emit_splinters() -> None:
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(20.0 + random.random() * 25),
|
|
scale=0.8,
|
|
spread=1.0,
|
|
chunk_type='splinter')
|
|
|
|
bs.timer(0.01, emit_splinters)
|
|
|
|
# Every now and then do a sparky one.
|
|
if self.blast_type == 'tnt' or random.random() < 0.1:
|
|
|
|
def emit_extra_sparks() -> None:
|
|
bs.emitfx(position=position,
|
|
velocity=velocity,
|
|
count=int(10.0 + random.random() * 20),
|
|
scale=0.8,
|
|
spread=1.5,
|
|
chunk_type='spark')
|
|
|
|
bs.timer(0.02, emit_extra_sparks)
|
|
|
|
# It looks better if we delay a bit.
|
|
bs.timer(0.05, emit)
|
|
|
|
lcolor = ((0.6, 0.6, 1.0) if self.blast_type == 'ice' else
|
|
(1, 0.3, 0.1))
|
|
light = bs.newnode('light',
|
|
attrs={
|
|
'position': position,
|
|
'volume_intensity_scale': 10.0,
|
|
'color': lcolor
|
|
})
|
|
|
|
scl = random.uniform(0.6, 0.9)
|
|
scorch_radius = light_radius = self.radius
|
|
if self.blast_type == 'tnt':
|
|
light_radius *= 1.4
|
|
scorch_radius *= 1.15
|
|
scl *= 3.0
|
|
|
|
iscale = 1.6
|
|
bs.animate(
|
|
light, 'intensity', {
|
|
0: 2.0 * iscale,
|
|
scl * 0.02: 0.1 * iscale,
|
|
scl * 0.025: 0.2 * iscale,
|
|
scl * 0.05: 17.0 * iscale,
|
|
scl * 0.06: 5.0 * iscale,
|
|
scl * 0.08: 4.0 * iscale,
|
|
scl * 0.2: 0.6 * iscale,
|
|
scl * 2.0: 0.00 * iscale,
|
|
scl * 3.0: 0.0
|
|
})
|
|
bs.animate(
|
|
light, 'radius', {
|
|
0: light_radius * 0.2,
|
|
scl * 0.05: light_radius * 0.55,
|
|
scl * 0.1: light_radius * 0.3,
|
|
scl * 0.3: light_radius * 0.15,
|
|
scl * 1.0: light_radius * 0.05
|
|
})
|
|
bs.timer(scl * 3.0, light.delete)
|
|
|
|
# Make a scorch that fades over time.
|
|
scorch = bs.newnode('scorch',
|
|
attrs={
|
|
'position': position,
|
|
'size': scorch_radius * 0.5,
|
|
'big': (self.blast_type == 'tnt')
|
|
})
|
|
if self.blast_type == 'ice':
|
|
scorch.color = (1, 1, 1.5)
|
|
|
|
bs.animate(scorch, 'presence', {3.000: 1, 13.000: 0})
|
|
bs.timer(13.0, scorch.delete)
|
|
|
|
if self.blast_type == 'ice':
|
|
factory.hiss_sound.play(position=light.position)
|
|
|
|
lpos = light.position
|
|
factory.random_explode_sound().play(position=lpos)
|
|
factory.debris_fall_sound.play(position=lpos)
|
|
|
|
bs.camerashake(0.0)
|
|
|
|
# TNT is more epic.
|
|
if self.blast_type == 'tnt':
|
|
factory.random_explode_sound().play(position=lpos)
|
|
|
|
def _extra_boom() -> None:
|
|
factory.random_explode_sound().play(position=lpos)
|
|
|
|
bs.timer(0.25, _extra_boom)
|
|
|
|
def _extra_debris_sound() -> None:
|
|
factory.debris_fall_sound.play(position=lpos)
|
|
factory.wood_debris_fall_sound.play(position=lpos)
|
|
|
|
bs.timer(0.4, _extra_debris_sound)
|
|
|
|
|
|
class NewBomb(Bomb):
|
|
|
|
def explode(self) -> None:
|
|
"""Blows up the bomb if it has not yet done so."""
|
|
if self._exploded:
|
|
return
|
|
self._exploded = True
|
|
if self.node:
|
|
blast = NewBlast(position=self.node.position,
|
|
velocity=self.node.velocity,
|
|
blast_radius=self.blast_radius,
|
|
blast_type=self.bomb_type,
|
|
source_player=babase.existing(self._source_player),
|
|
hit_type=self.hit_type,
|
|
hit_subtype=self.hit_subtype).autoretain()
|
|
for callback in self._explode_callbacks:
|
|
callback(self, blast)
|
|
|
|
# We blew up so we need to go away.
|
|
# NOTE TO SELF: do we actually need this delay?
|
|
bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage()))
|
|
|
|
|
|
class TNT(bs.Actor):
|
|
|
|
def __init__(self,
|
|
position: Sequence[float] = (0.0, 1.0, 0.0),
|
|
velocity: Sequence[float] = (0.0, 0.0, 0.0),
|
|
tnt_scale: float = 1.0,
|
|
teleport: bool = True):
|
|
super().__init__()
|
|
self.position = position
|
|
self.teleport = teleport
|
|
|
|
self._no_collide_material = bs.Material()
|
|
self._no_collide_material.add_actions(
|
|
actions=('modify_part_collision', 'collide', False),
|
|
)
|
|
self._collide_material = bs.Material()
|
|
self._collide_material.add_actions(
|
|
actions=('modify_part_collision', 'collide', True),
|
|
)
|
|
|
|
if teleport:
|
|
collide = self._collide_material
|
|
else:
|
|
collide = self._no_collide_material
|
|
self.node = bs.newnode(
|
|
'prop',
|
|
delegate=self,
|
|
attrs={
|
|
'position': position,
|
|
'velocity': velocity,
|
|
'mesh': bs.getmesh('tnt'),
|
|
'color_texture': bs.gettexture('tnt'),
|
|
'body': 'crate',
|
|
'mesh_scale': tnt_scale,
|
|
'body_scale': tnt_scale,
|
|
'density': 2.0,
|
|
'gravity_scale': 2.0,
|
|
'materials': [collide]
|
|
}
|
|
)
|
|
if not teleport:
|
|
bs.timer(0.1, self._collide)
|
|
|
|
def _collide(self) -> None:
|
|
self.node.materials += (self._collide_material,)
|
|
|
|
def handlemessage(self, msg: Any) -> Any:
|
|
if isinstance(msg, bs.OutOfBoundsMessage):
|
|
if self.teleport:
|
|
self.node.position = self.position
|
|
self.node.velocity = (0, 0, 0)
|
|
else:
|
|
self.node.delete()
|
|
else:
|
|
super().handlemessage(msg)
|
|
|
|
|
|
class RaceRegion(bs.Actor):
|
|
"""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
|
|
self.node = bs.newnode(
|
|
'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]
|
|
})
|
|
|
|
|
|
# MINIGAME
|
|
class Player(bs.Player['Team']):
|
|
"""Our player type for this game."""
|
|
|
|
def __init__(self) -> None:
|
|
self.distance_txt: Optional[bs.Node] = None
|
|
self.last_region = 0
|
|
self.lap = 0
|
|
self.distance = 0.0
|
|
self.finished = False
|
|
self.rank: Optional[int] = None
|
|
|
|
|
|
class Team(bs.Team[Player]):
|
|
"""Our team type for this game."""
|
|
|
|
def __init__(self) -> None:
|
|
self.time: Optional[float] = None
|
|
self.lap = 0
|
|
self.finished = False
|
|
|
|
|
|
# ba_meta export bascenev1.GameActivity
|
|
class RaceGame(bs.TeamGameActivity[Player, Team]):
|
|
"""Game of racing around a track."""
|
|
|
|
name = 'Hyper Race'
|
|
description = 'Creado Por Cebolla!!'
|
|
scoreconfig = bs.ScoreConfig(label='Time',
|
|
lower_is_better=True,
|
|
scoretype=bs.ScoreType.MILLISECONDS)
|
|
|
|
@classmethod
|
|
def get_available_settings(
|
|
cls, sessiontype: Type[bs.Session]) -> List[babase.Setting]:
|
|
settings = [
|
|
bs.IntSetting('Laps', min_value=1, default=3, increment=1),
|
|
bs.IntChoiceSetting(
|
|
'Time Limit',
|
|
default=0,
|
|
choices=[
|
|
('None', 0),
|
|
('1 Minute', 60),
|
|
('2 Minutes', 120),
|
|
('5 Minutes', 300),
|
|
('10 Minutes', 600),
|
|
('20 Minutes', 1200),
|
|
],
|
|
),
|
|
bs.BoolSetting('Epic Mode', default=False),
|
|
]
|
|
|
|
# We have some specific settings in teams mode.
|
|
if issubclass(sessiontype, bs.DualTeamSession):
|
|
settings.append(
|
|
bs.BoolSetting('Entire Team Must Finish', default=False))
|
|
return settings
|
|
|
|
@classmethod
|
|
def supports_session_type(cls, sessiontype: Type[bs.Session]) -> bool:
|
|
return issubclass(sessiontype, bs.MultiTeamSession)
|
|
|
|
@classmethod
|
|
def get_supported_maps(cls, sessiontype: Type[bs.Session]) -> List[str]:
|
|
return bs.app.classic.getmaps('hyper')
|
|
|
|
def __init__(self, settings: dict):
|
|
self._race_started = False
|
|
super().__init__(settings)
|
|
self.factory = factory = BombFactory.get()
|
|
self.shared = shared = SharedObjects.get()
|
|
self._scoreboard = Scoreboard()
|
|
self._score_sound = bs.getsound('score')
|
|
self._swipsound = bs.getsound('swip')
|
|
self._last_team_time: Optional[float] = None
|
|
self._front_race_region: Optional[int] = None
|
|
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
|
|
self._regions: List[RaceRegion] = []
|
|
self._team_finish_pts: Optional[int] = None
|
|
self._time_text: Optional[bs.Actor] = None
|
|
self._timer: Optional[OnScreenTimer] = 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._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._epic_mode = bool(settings['Epic Mode'])
|
|
|
|
# Base class overrides.
|
|
self.slow_motion = self._epic_mode
|
|
self.default_music = (bs.MusicType.EPIC_RACE
|
|
if self._epic_mode else bs.MusicType.RACE)
|
|
|
|
self._safe_region_material = bs.Material()
|
|
self._safe_region_material.add_actions(
|
|
conditions=('they_have_material', shared.player_material),
|
|
actions=(('modify_part_collision', 'collide', True),
|
|
('modify_part_collision', 'physical', True))
|
|
)
|
|
|
|
def get_instance_description(self) -> Union[str, Sequence]:
|
|
if (isinstance(self.session, bs.DualTeamSession)
|
|
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')
|
|
mat = self.race_region_material = bs.Material()
|
|
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),
|
|
))
|
|
for rpt in pts:
|
|
self._regions.append(RaceRegion(rpt, len(self._regions)))
|
|
|
|
bs.newnode(
|
|
'region',
|
|
attrs={
|
|
'position': (0.3, 4.044276501, -2.9),
|
|
'scale': (11.7, 15, 9.5),
|
|
'type': 'box',
|
|
'materials': [self._safe_region_material]
|
|
}
|
|
)
|
|
|
|
def _flash_player(self, player: Player, scale: float) -> None:
|
|
assert isinstance(player.actor, PlayerSpaz)
|
|
assert player.actor.node
|
|
pos = player.actor.node.position
|
|
light = bs.newnode('light',
|
|
attrs={
|
|
'position': pos,
|
|
'color': (1, 1, 0),
|
|
'height_attenuated': False,
|
|
'radius': 0.4
|
|
})
|
|
bs.timer(0.5, light.delete)
|
|
bs.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0})
|
|
|
|
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
|
|
collision = bs.getcollision()
|
|
try:
|
|
region = collision.sourcenode.getdelegate(RaceRegion, True)
|
|
spaz = collision.opposingnode.getdelegate(PlayerSpaz,True)
|
|
except bs.NotFoundError:
|
|
return
|
|
|
|
if not spaz.is_alive():
|
|
return
|
|
|
|
try:
|
|
player = spaz.getplayer(Player, True)
|
|
except bs.NotFoundError:
|
|
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
|
|
player.actor.handlemessage(bs.DieMessage())
|
|
bs.broadcastmessage(babase.Lstr(
|
|
translate=('statements', 'Killing ${NAME} for'
|
|
' skipping part of the track!'),
|
|
subs=[('${NAME}', player.getname(full=True))]),
|
|
color=(1, 0, 0))
|
|
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.
|
|
if isinstance(self.session, bs.DualTeamSession
|
|
) 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.
|
|
if isinstance(self.session, bs.DualTeamSession):
|
|
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(
|
|
bs.DieMessage(immediate=True))
|
|
|
|
# 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:
|
|
self._score_sound.play()
|
|
player.team.finished = True
|
|
assert self._timer is not None
|
|
elapsed = bs.time() - self._timer.getstarttime()
|
|
self._last_team_time = player.team.time = elapsed
|
|
self._check_end_game()
|
|
|
|
# Team has yet to finish.
|
|
else:
|
|
self._swipsound.play()
|
|
|
|
# They've just finished a lap but not the race.
|
|
else:
|
|
self._swipsound.play()
|
|
self._flash_player(player, 0.3)
|
|
|
|
# Print their lap number over their head.
|
|
try:
|
|
assert isinstance(player.actor, PlayerSpaz)
|
|
mathnode = bs.newnode('math',
|
|
owner=player.actor.node,
|
|
attrs={
|
|
'input1': (0, 1.9, 0),
|
|
'operation': 'add'
|
|
})
|
|
player.actor.node.connectattr(
|
|
'torso_position', mathnode, 'input2')
|
|
tstr = babase.Lstr(resource='lapNumberText',
|
|
subs=[('${CURRENT}',
|
|
str(player.lap + 1)),
|
|
('${TOTAL}', str(self._laps))
|
|
])
|
|
txtnode = bs.newnode('text',
|
|
owner=mathnode,
|
|
attrs={
|
|
'text': tstr,
|
|
'in_world': True,
|
|
'color': (1, 1, 0, 1),
|
|
'scale': 0.015,
|
|
'h_align': 'center'
|
|
})
|
|
mathnode.connectattr('output', txtnode, 'position')
|
|
bs.animate(txtnode, 'scale', {
|
|
0.0: 0,
|
|
0.2: 0.019,
|
|
2.0: 0.019,
|
|
2.2: 0
|
|
})
|
|
bs.timer(2.3, mathnode.delete)
|
|
except Exception:
|
|
babase.print_exception('Error printing lap.')
|
|
|
|
def on_team_join(self, team: Team) -> None:
|
|
self._update_scoreboard()
|
|
|
|
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).
|
|
if (isinstance(self.session, bs.DualTeamSession)
|
|
and self._entire_team_must_finish):
|
|
bs.broadcastmessage(babase.Lstr(
|
|
translate=('statements',
|
|
'${TEAM} is disqualified because ${PLAYER} left'),
|
|
subs=[('${TEAM}', player.team.name),
|
|
('${PLAYER}', player.getname(full=True))]),
|
|
color=(1, 1, 0))
|
|
player.team.finished = True
|
|
player.team.time = None
|
|
player.team.lap = 0
|
|
bs.getsound('boo').play()
|
|
for otherplayer in player.team.players:
|
|
otherplayer.lap = 0
|
|
otherplayer.finished = True
|
|
try:
|
|
if otherplayer.actor is not None:
|
|
otherplayer.actor.handlemessage(bs.DieMessage())
|
|
except Exception:
|
|
babase.print_exception('Error sending DieMessage.')
|
|
|
|
# Defer so team/player lists will be updated.
|
|
babase.pushcall(self._check_end_game)
|
|
|
|
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:
|
|
if (isinstance(self.session, bs.DualTeamSession)
|
|
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:
|
|
from bascenev1lib.actor.onscreentimer import OnScreenTimer
|
|
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.
|
|
self._time_text = bs.NodeActor(
|
|
bs.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.4,
|
|
'text': ''
|
|
}))
|
|
self._timer = OnScreenTimer()
|
|
|
|
self._scoreboard_timer = bs.Timer(0.25,
|
|
self._update_scoreboard,
|
|
repeat=True)
|
|
self._player_order_update_timer = bs.Timer(0.25,
|
|
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
|
|
|
|
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)
|
|
|
|
self._start_lights = []
|
|
for i in range(4):
|
|
lnub = bs.newnode('image',
|
|
attrs={
|
|
'texture': bs.gettexture('nub'),
|
|
'opacity': 1.0,
|
|
'absolute_scale': True,
|
|
'position': (-75 + i * 50, light_y),
|
|
'scale': (50, 50),
|
|
'attach': 'center'
|
|
})
|
|
bs.animate(
|
|
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
|
|
})
|
|
bs.timer(13.0 * t_scale, lnub.delete)
|
|
self._start_lights.append(lnub)
|
|
|
|
self._obstacles()
|
|
|
|
pts = self.map.get_def_points('race_point')
|
|
for rpt in pts:
|
|
bs.newnode(
|
|
'locator',
|
|
attrs={
|
|
'shape': 'circle',
|
|
'position': (rpt[0], 4.382076338, rpt[2]),
|
|
'size': (rpt[3] * 2.0, 0, rpt[5] * 2.0),
|
|
'color': (0, 1, 0),
|
|
'opacity': 1.0,
|
|
'draw_beauty': False,
|
|
'additive': True
|
|
}
|
|
)
|
|
|
|
def _obstacles(self) -> None:
|
|
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._tnt((1.5, 5, 2.3), (0, 0, 0), 1.0)
|
|
self._tnt((1.5, 5, 3.3), (0, 0, 0), 1.0)
|
|
|
|
self._tnt((3.5, 5, 2.3), (0, 0, 0), 1.0)
|
|
self._tnt((3.5, 5, 3.3), (0, 0, 0), 1.0)
|
|
|
|
self._tnt((5.5, 5, 2.3), (0, 0, 0), 1.0)
|
|
self._tnt((5.5, 5, 3.3), (0, 0, 0), 1.0)
|
|
|
|
self._tnt((-6, 5, -7), (0, 0, 0), 1.3)
|
|
self._tnt((-7, 5, -5), (0, 0, 0), 1.3)
|
|
self._tnt((-6, 5, -3), (0, 0, 0), 1.3)
|
|
self._tnt((-7, 5, -1), (0, 0, 0), 1.3)
|
|
self._tnt((-6, 5, 1), (0, 0, 0), 1.3)
|
|
|
|
bs.timer(0.1, bs.WeakCall(self._tnt, (-3.2, 5, 1),
|
|
(0, 0, 0), 1.0, (0, 20, 60)), repeat=True)
|
|
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(6, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(6.8, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(7.6, 7, 1), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(6, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(6.8, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(7.6, 7, -2.2), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(6, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(6.8, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(7.6, 7, -5.2), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(6, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(6.8, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(7.6, 7, -8), (0, 0, 0), 1.0, 1.0), repeat=True)
|
|
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(-5, 5, 0), (0, 0, 0), 1.0, 1.0, (0, 20, 3)), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'impact',
|
|
(-1.5, 5, 0), (0, 0, 0), 1.0, 1.0, (0, 20, 3)), repeat=True)
|
|
|
|
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky',
|
|
(-1, 5, -8), (0, 10, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky',
|
|
(-1, 5, -9), (0, 10, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky',
|
|
(-1, 5, -10), (0, 10, 0), 1.0, 1.0), repeat=True)
|
|
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky',
|
|
(-4.6, 5, -8), (0, 10, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky',
|
|
(-4.6, 5, -9), (0, 10, 0), 1.0, 1.0), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(self._bomb, 'sticky',
|
|
(-4.6, 5, -10), (0, 10, 0), 1.0, 1.0), repeat=True)
|
|
|
|
bs.timer(1.6, bs.WeakCall(
|
|
self._powerup, (2, 5, -5), 'curse', (0, 20, -3)), repeat=True)
|
|
bs.timer(1.6, bs.WeakCall(
|
|
self._powerup, (4, 5, -5), 'curse', (0, 20, -3)), repeat=True)
|
|
|
|
def _tnt(self,
|
|
position: float,
|
|
velocity: float,
|
|
tnt_scale: float,
|
|
extra_acceleration: float = None) -> None:
|
|
if extra_acceleration:
|
|
TNT(position, velocity, tnt_scale, False).autoretain(
|
|
).node.extra_acceleration = extra_acceleration
|
|
else:
|
|
TNT(position, velocity, tnt_scale).autoretain()
|
|
|
|
def _bomb(self,
|
|
type: str,
|
|
position: float,
|
|
velocity: float,
|
|
mesh_scale: float,
|
|
body_scale: float,
|
|
extra_acceleration: float = None) -> None:
|
|
if extra_acceleration:
|
|
NewBomb(position=position,
|
|
velocity=velocity,
|
|
bomb_type=type).autoretain(
|
|
).node.extra_acceleration = extra_acceleration
|
|
else:
|
|
NewBomb(position=position,
|
|
velocity=velocity,
|
|
bomb_type=type).autoretain()
|
|
|
|
def _powerup(self,
|
|
position: float,
|
|
poweruptype: str,
|
|
extra_acceleration: float = None) -> None:
|
|
if extra_acceleration:
|
|
PowerupBox(position=position,
|
|
poweruptype=poweruptype).autoretain(
|
|
).node.extra_acceleration = extra_acceleration
|
|
else:
|
|
PowerupBox(position=position, poweruptype=poweruptype).autoretain()
|
|
|
|
def _do_light_1(self) -> None:
|
|
assert self._start_lights is not None
|
|
self._start_lights[0].color = (1.0, 0, 0)
|
|
self._beep_1_sound.play()
|
|
|
|
def _do_light_2(self) -> None:
|
|
assert self._start_lights is not None
|
|
self._start_lights[1].color = (1.0, 0, 0)
|
|
self._beep_1_sound.play()
|
|
|
|
def _do_light_3(self) -> None:
|
|
assert self._start_lights is not None
|
|
self._start_lights[2].color = (1.0, 0.3, 0)
|
|
self._beep_1_sound.play()
|
|
|
|
def _start_race(self) -> None:
|
|
assert self._start_lights is not None
|
|
self._start_lights[3].color = (0.0, 1.0, 0)
|
|
self._beep_2_sound.play()
|
|
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:
|
|
babase.print_exception('Error in race player connects.')
|
|
assert self._timer is not None
|
|
self._timer.start()
|
|
|
|
self._race_started = True
|
|
|
|
def _update_player_order(self) -> None:
|
|
|
|
# Calc all player distances.
|
|
for player in self.players:
|
|
pos: Optional[babase.Vec3]
|
|
try:
|
|
pos = player.position
|
|
except bs.NotFoundError:
|
|
pos = None
|
|
if pos is not None:
|
|
r_index = player.last_region
|
|
rg1 = self._regions[r_index]
|
|
r1pt = babase.Vec3(rg1.pos[:3])
|
|
rg2 = self._regions[0] if r_index == len(
|
|
self._regions) - 1 else self._regions[r_index + 1]
|
|
r2pt = babase.Vec3(rg2.pos[:3])
|
|
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_player(self, player: Player) -> bs.Actor:
|
|
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()
|
|
|
|
mathnode = bs.newnode('math',
|
|
owner=spaz.node,
|
|
attrs={
|
|
'input1': (0, 1.4, 0),
|
|
'operation': 'add'
|
|
})
|
|
spaz.node.connectattr('torso_position', mathnode, 'input2')
|
|
|
|
distance_txt = bs.newnode('text',
|
|
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.
|
|
if isinstance(session, bs.DualTeamSession):
|
|
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.
|
|
assert isinstance(session, bs.FreeForAllSession)
|
|
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))
|
|
|
|
results = bs.GameResults()
|
|
|
|
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,
|
|
bs.DualTeamSession))
|
|
|
|
def handlemessage(self, msg: Any) -> Any:
|
|
if isinstance(msg, bs.PlayerDiedMessage):
|
|
# Augment default behavior.
|
|
super().handlemessage(msg)
|
|
player = msg.getplayer(Player)
|
|
if not player.finished:
|
|
self.respawn_player(player, respawn_time=1)
|
|
else:
|
|
super().handlemessage(msg)
|