mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2026-08-15 13:04:30 +00:00
hello API 8 !
This commit is contained in:
parent
3a2b6ade68
commit
0284fee95c
1166 changed files with 26061 additions and 375100 deletions
2
dist/ba_data/python/bascenev1lib/game/__init__.py
vendored
Normal file
2
dist/ba_data/python/bascenev1lib/game/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
"""Our lovely collection of game related modules."""
|
||||
262
dist/ba_data/python/bascenev1lib/game/assault.py
vendored
Normal file
262
dist/ba_data/python/bascenev1lib/game/assault.py
vendored
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Defines assault minigame."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.actor.flag import Flag
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.gameutils import SharedObjects
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self, base_pos: Sequence[float], flag: Flag) -> None:
|
||||
self.base_pos = base_pos
|
||||
self.flag = flag
|
||||
self.score = 0
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class AssaultGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""Game where you score by touching the other team's flag."""
|
||||
|
||||
name = 'Assault'
|
||||
description = 'Reach the enemy flag to score.'
|
||||
available_settings = [
|
||||
bs.IntSetting(
|
||||
'Score to Win',
|
||||
min_value=1,
|
||||
default=3,
|
||||
),
|
||||
bs.IntChoiceSetting(
|
||||
'Time Limit',
|
||||
choices=[
|
||||
('None', 0),
|
||||
('1 Minute', 60),
|
||||
('2 Minutes', 120),
|
||||
('5 Minutes', 300),
|
||||
('10 Minutes', 600),
|
||||
('20 Minutes', 1200),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
bs.FloatChoiceSetting(
|
||||
'Respawn Times',
|
||||
choices=[
|
||||
('Shorter', 0.25),
|
||||
('Short', 0.5),
|
||||
('Normal', 1.0),
|
||||
('Long', 2.0),
|
||||
('Longer', 4.0),
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
bs.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
return issubclass(sessiontype, bs.DualTeamSession)
|
||||
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
assert bs.app.classic is not None
|
||||
return bs.app.classic.getmaps('team_flag')
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
self._scoreboard = Scoreboard()
|
||||
self._last_score_time = 0.0
|
||||
self._score_sound = bs.getsound('score')
|
||||
self._base_region_materials: dict[int, bs.Material] = {}
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self._score_to_win = int(settings['Score to Win'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
|
||||
# Base class overrides
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH
|
||||
)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
if self._score_to_win == 1:
|
||||
return 'Touch the enemy flag.'
|
||||
return 'Touch the enemy flag ${ARG1} times.', self._score_to_win
|
||||
|
||||
def get_instance_description_short(self) -> str | Sequence:
|
||||
if self._score_to_win == 1:
|
||||
return 'touch 1 flag'
|
||||
return 'touch ${ARG1} flags', self._score_to_win
|
||||
|
||||
def create_team(self, sessionteam: bs.SessionTeam) -> Team:
|
||||
shared = SharedObjects.get()
|
||||
base_pos = self.map.get_flag_position(sessionteam.id)
|
||||
bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': base_pos,
|
||||
'intensity': 0.6,
|
||||
'height_attenuated': False,
|
||||
'volume_intensity_scale': 0.1,
|
||||
'radius': 0.1,
|
||||
'color': sessionteam.color,
|
||||
},
|
||||
)
|
||||
Flag.project_stand(base_pos)
|
||||
flag = Flag(touchable=False, position=base_pos, color=sessionteam.color)
|
||||
team = Team(base_pos=base_pos, flag=flag)
|
||||
|
||||
mat = self._base_region_materials[sessionteam.id] = 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',
|
||||
bs.Call(self._handle_base_collide, team),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
bs.newnode(
|
||||
'region',
|
||||
owner=flag.node,
|
||||
attrs={
|
||||
'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]),
|
||||
'scale': (0.5, 0.5, 0.5),
|
||||
'type': 'sphere',
|
||||
'materials': [self._base_region_materials[sessionteam.id]],
|
||||
},
|
||||
)
|
||||
|
||||
return team
|
||||
|
||||
def on_team_join(self, team: Team) -> None:
|
||||
# Can't do this in create_team because the team's color/etc. have
|
||||
# not been wired up yet at that point.
|
||||
self._update_scoreboard()
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
self.setup_standard_time_limit(self._time_limit)
|
||||
self.setup_standard_powerup_drops()
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
super().handlemessage(msg) # Augment standard.
|
||||
self.respawn_player(msg.getplayer(Player))
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
|
||||
def _flash_base(self, team: Team, length: float = 2.0) -> None:
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': team.base_pos,
|
||||
'height_attenuated': False,
|
||||
'radius': 0.3,
|
||||
'color': team.color,
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
|
||||
bs.timer(length, light.delete)
|
||||
|
||||
def _handle_base_collide(self, team: Team) -> None:
|
||||
try:
|
||||
spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
|
||||
except bs.NotFoundError:
|
||||
return
|
||||
|
||||
if not spaz.is_alive():
|
||||
return
|
||||
|
||||
try:
|
||||
player = spaz.getplayer(Player, True)
|
||||
except bs.NotFoundError:
|
||||
return
|
||||
|
||||
# If its another team's player, they scored.
|
||||
player_team = player.team
|
||||
if player_team is not team:
|
||||
# Prevent multiple simultaneous scores.
|
||||
if bs.time() != self._last_score_time:
|
||||
self._last_score_time = bs.time()
|
||||
self.stats.player_scored(player, 50, big_message=True)
|
||||
self._score_sound.play()
|
||||
self._flash_base(team)
|
||||
|
||||
# Move all players on the scoring team back to their start
|
||||
# and add flashes of light so its noticeable.
|
||||
for player in player_team.players:
|
||||
if player.is_alive():
|
||||
pos = player.node.position
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': pos,
|
||||
'color': player_team.color,
|
||||
'height_attenuated': False,
|
||||
'radius': 0.4,
|
||||
},
|
||||
)
|
||||
bs.timer(0.5, light.delete)
|
||||
bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
|
||||
|
||||
new_pos = self.map.get_start_position(player_team.id)
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': new_pos,
|
||||
'color': player_team.color,
|
||||
'radius': 0.4,
|
||||
'height_attenuated': False,
|
||||
},
|
||||
)
|
||||
bs.timer(0.5, light.delete)
|
||||
bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
|
||||
if player.actor:
|
||||
player.actor.handlemessage(
|
||||
bs.StandMessage(new_pos, random.uniform(0, 360))
|
||||
)
|
||||
|
||||
# Have teammates celebrate.
|
||||
for player in player_team.players:
|
||||
if player.actor:
|
||||
player.actor.handlemessage(bs.CelebrateMessage(2.0))
|
||||
|
||||
player_team.score += 1
|
||||
self._update_scoreboard()
|
||||
if player_team.score >= self._score_to_win:
|
||||
self.end_game()
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = bs.GameResults()
|
||||
for team in self.teams:
|
||||
results.set_team_score(team, team.score)
|
||||
self.end(results=results)
|
||||
|
||||
def _update_scoreboard(self) -> None:
|
||||
for team in self.teams:
|
||||
self._scoreboard.set_team_value(
|
||||
team, team.score, self._score_to_win
|
||||
)
|
||||
610
dist/ba_data/python/bascenev1lib/game/capturetheflag.py
vendored
Normal file
610
dist/ba_data/python/bascenev1lib/game/capturetheflag.py
vendored
Normal file
|
|
@ -0,0 +1,610 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Defines a capture-the-flag game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.actor.flag import (
|
||||
FlagFactory,
|
||||
Flag,
|
||||
FlagPickedUpMessage,
|
||||
FlagDroppedMessage,
|
||||
FlagDiedMessage,
|
||||
)
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class CTFFlag(Flag):
|
||||
"""Special flag type for CTF games."""
|
||||
|
||||
activity: CaptureTheFlagGame
|
||||
|
||||
def __init__(self, team: Team):
|
||||
assert team.flagmaterial is not None
|
||||
super().__init__(
|
||||
materials=[team.flagmaterial],
|
||||
position=team.base_pos,
|
||||
color=team.color,
|
||||
)
|
||||
self._team = team
|
||||
self.held_count = 0
|
||||
self.counter = bs.newnode(
|
||||
'text',
|
||||
owner=self.node,
|
||||
attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'},
|
||||
)
|
||||
self.reset_return_times()
|
||||
self.last_player_to_hold: Player | None = None
|
||||
self.time_out_respawn_time: int | None = None
|
||||
self.touch_return_time: float | None = None
|
||||
|
||||
def reset_return_times(self) -> None:
|
||||
"""Clear flag related times in the activity."""
|
||||
self.time_out_respawn_time = int(self.activity.flag_idle_return_time)
|
||||
self.touch_return_time = float(self.activity.flag_touch_return_time)
|
||||
|
||||
@property
|
||||
def team(self) -> Team:
|
||||
"""The flag's team."""
|
||||
return self._team
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.touching_own_flag = 0
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_pos: Sequence[float],
|
||||
base_region_material: bs.Material,
|
||||
base_region: bs.Node,
|
||||
spaz_material_no_flag_physical: bs.Material,
|
||||
spaz_material_no_flag_collide: bs.Material,
|
||||
flagmaterial: bs.Material,
|
||||
):
|
||||
self.base_pos = base_pos
|
||||
self.base_region_material = base_region_material
|
||||
self.base_region = base_region
|
||||
self.spaz_material_no_flag_physical = spaz_material_no_flag_physical
|
||||
self.spaz_material_no_flag_collide = spaz_material_no_flag_collide
|
||||
self.flagmaterial = flagmaterial
|
||||
self.score = 0
|
||||
self.flag_return_touches = 0
|
||||
self.home_flag_at_base = True
|
||||
self.touch_return_timer: bs.Timer | None = None
|
||||
self.enemy_flag_at_base = False
|
||||
self.flag: CTFFlag | None = None
|
||||
self.last_flag_leave_time: float | None = None
|
||||
self.touch_return_timer_ticking: bs.NodeActor | None = None
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""Game of stealing other team's flag and returning it to your base."""
|
||||
|
||||
name = 'Capture the Flag'
|
||||
description = 'Return the enemy flag to score.'
|
||||
available_settings = [
|
||||
bs.IntSetting('Score to Win', min_value=1, default=3),
|
||||
bs.IntSetting(
|
||||
'Flag Touch Return Time',
|
||||
min_value=0,
|
||||
default=0,
|
||||
increment=1,
|
||||
),
|
||||
bs.IntSetting(
|
||||
'Flag Idle Return Time',
|
||||
min_value=5,
|
||||
default=30,
|
||||
increment=5,
|
||||
),
|
||||
bs.IntChoiceSetting(
|
||||
'Time Limit',
|
||||
choices=[
|
||||
('None', 0),
|
||||
('1 Minute', 60),
|
||||
('2 Minutes', 120),
|
||||
('5 Minutes', 300),
|
||||
('10 Minutes', 600),
|
||||
('20 Minutes', 1200),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
bs.FloatChoiceSetting(
|
||||
'Respawn Times',
|
||||
choices=[
|
||||
('Shorter', 0.25),
|
||||
('Short', 0.5),
|
||||
('Normal', 1.0),
|
||||
('Long', 2.0),
|
||||
('Longer', 4.0),
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
bs.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
return issubclass(sessiontype, bs.DualTeamSession)
|
||||
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
assert bs.app.classic is not None
|
||||
return bs.app.classic.getmaps('team_flag')
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
self._scoreboard = Scoreboard()
|
||||
self._alarmsound = bs.getsound('alarm')
|
||||
self._ticking_sound = bs.getsound('ticking')
|
||||
self._score_sound = bs.getsound('score')
|
||||
self._swipsound = bs.getsound('swip')
|
||||
self._last_score_time = 0
|
||||
self._all_bases_material = bs.Material()
|
||||
self._last_home_flag_notice_print_time = 0.0
|
||||
self._score_to_win = int(settings['Score to Win'])
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
|
||||
self.flag_touch_return_time = float(settings['Flag Touch Return Time'])
|
||||
self.flag_idle_return_time = float(settings['Flag Idle Return Time'])
|
||||
|
||||
# Base class overrides.
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FLAG_CATCHER
|
||||
)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
if self._score_to_win == 1:
|
||||
return 'Steal the enemy flag.'
|
||||
return 'Steal the enemy flag ${ARG1} times.', self._score_to_win
|
||||
|
||||
def get_instance_description_short(self) -> str | Sequence:
|
||||
if self._score_to_win == 1:
|
||||
return 'return 1 flag'
|
||||
return 'return ${ARG1} flags', self._score_to_win
|
||||
|
||||
def create_team(self, sessionteam: bs.SessionTeam) -> Team:
|
||||
# Create our team instance and its initial values.
|
||||
|
||||
base_pos = self.map.get_flag_position(sessionteam.id)
|
||||
Flag.project_stand(base_pos)
|
||||
|
||||
bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': base_pos,
|
||||
'intensity': 0.6,
|
||||
'height_attenuated': False,
|
||||
'volume_intensity_scale': 0.1,
|
||||
'radius': 0.1,
|
||||
'color': sessionteam.color,
|
||||
},
|
||||
)
|
||||
|
||||
base_region_mat = bs.Material()
|
||||
pos = base_pos
|
||||
base_region = bs.newnode(
|
||||
'region',
|
||||
attrs={
|
||||
'position': (pos[0], pos[1] + 0.75, pos[2]),
|
||||
'scale': (0.5, 0.5, 0.5),
|
||||
'type': 'sphere',
|
||||
'materials': [base_region_mat, self._all_bases_material],
|
||||
},
|
||||
)
|
||||
|
||||
spaz_mat_no_flag_physical = bs.Material()
|
||||
spaz_mat_no_flag_collide = bs.Material()
|
||||
flagmat = bs.Material()
|
||||
|
||||
team = Team(
|
||||
base_pos=base_pos,
|
||||
base_region_material=base_region_mat,
|
||||
base_region=base_region,
|
||||
spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
|
||||
spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
|
||||
flagmaterial=flagmat,
|
||||
)
|
||||
|
||||
# Some parts of our spazzes don't collide physically with our
|
||||
# flags but generate callbacks.
|
||||
spaz_mat_no_flag_physical.add_actions(
|
||||
conditions=('they_have_material', flagmat),
|
||||
actions=(
|
||||
('modify_part_collision', 'physical', False),
|
||||
(
|
||||
'call',
|
||||
'at_connect',
|
||||
lambda: self._handle_touching_own_flag(team, True),
|
||||
),
|
||||
(
|
||||
'call',
|
||||
'at_disconnect',
|
||||
lambda: self._handle_touching_own_flag(team, False),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# Other parts of our spazzes don't collide with our flags at all.
|
||||
spaz_mat_no_flag_collide.add_actions(
|
||||
conditions=('they_have_material', flagmat),
|
||||
actions=('modify_part_collision', 'collide', False),
|
||||
)
|
||||
|
||||
# We wanna know when *any* flag enters/leaves our base.
|
||||
base_region_mat.add_actions(
|
||||
conditions=('they_have_material', FlagFactory.get().flagmaterial),
|
||||
actions=(
|
||||
('modify_part_collision', 'collide', True),
|
||||
('modify_part_collision', 'physical', False),
|
||||
(
|
||||
'call',
|
||||
'at_connect',
|
||||
lambda: self._handle_flag_entered_base(team),
|
||||
),
|
||||
(
|
||||
'call',
|
||||
'at_disconnect',
|
||||
lambda: self._handle_flag_left_base(team),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return team
|
||||
|
||||
def on_team_join(self, team: Team) -> None:
|
||||
# Can't do this in create_team because the team's color/etc. have
|
||||
# not been wired up yet at that point.
|
||||
self._spawn_flag_for_team(team)
|
||||
self._update_scoreboard()
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
self.setup_standard_time_limit(self._time_limit)
|
||||
self.setup_standard_powerup_drops()
|
||||
bs.timer(1.0, call=self._tick, repeat=True)
|
||||
|
||||
def _spawn_flag_for_team(self, team: Team) -> None:
|
||||
team.flag = CTFFlag(team)
|
||||
team.flag_return_touches = 0
|
||||
self._flash_base(team, length=1.0)
|
||||
assert team.flag.node
|
||||
self._swipsound.play(position=team.flag.node.position)
|
||||
|
||||
def _handle_flag_entered_base(self, team: Team) -> None:
|
||||
try:
|
||||
flag = bs.getcollision().opposingnode.getdelegate(CTFFlag, True)
|
||||
except bs.NotFoundError:
|
||||
# Don't think this should logically ever happen.
|
||||
print('Error getting CTFFlag in entering-base callback.')
|
||||
return
|
||||
|
||||
if flag.team is team:
|
||||
team.home_flag_at_base = True
|
||||
|
||||
# If the enemy flag is already here, score!
|
||||
if team.enemy_flag_at_base:
|
||||
# And show team name which scored (but actually we could
|
||||
# show here player who returned enemy flag).
|
||||
self.show_zoom_message(
|
||||
bs.Lstr(
|
||||
resource='nameScoresText', subs=[('${NAME}', team.name)]
|
||||
),
|
||||
color=team.color,
|
||||
)
|
||||
self._score(team)
|
||||
else:
|
||||
team.enemy_flag_at_base = True
|
||||
if team.home_flag_at_base:
|
||||
# Award points to whoever was carrying the enemy flag.
|
||||
player = flag.last_player_to_hold
|
||||
if player and player.team is team:
|
||||
self.stats.player_scored(player, 50, big_message=True)
|
||||
|
||||
# Update score and reset flags.
|
||||
self._score(team)
|
||||
|
||||
# If the home-team flag isn't here, print a message to that effect.
|
||||
else:
|
||||
# Don't want slo-mo affecting this
|
||||
curtime = bs.basetime()
|
||||
if curtime - self._last_home_flag_notice_print_time > 5.0:
|
||||
self._last_home_flag_notice_print_time = curtime
|
||||
bpos = team.base_pos
|
||||
tval = bs.Lstr(resource='ownFlagAtYourBaseWarning')
|
||||
tnode = bs.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'text': tval,
|
||||
'in_world': True,
|
||||
'scale': 0.013,
|
||||
'color': (1, 1, 0, 1),
|
||||
'h_align': 'center',
|
||||
'position': (bpos[0], bpos[1] + 3.2, bpos[2]),
|
||||
},
|
||||
)
|
||||
bs.timer(5.1, tnode.delete)
|
||||
bs.animate(
|
||||
tnode, 'scale', {0.0: 0, 0.2: 0.013, 4.8: 0.013, 5.0: 0}
|
||||
)
|
||||
|
||||
def _tick(self) -> None:
|
||||
# If either flag is away from base and not being held, tick down its
|
||||
# respawn timer.
|
||||
for team in self.teams:
|
||||
flag = team.flag
|
||||
assert flag is not None
|
||||
|
||||
if not team.home_flag_at_base and flag.held_count == 0:
|
||||
time_out_counting_down = True
|
||||
if flag.time_out_respawn_time is None:
|
||||
flag.reset_return_times()
|
||||
assert flag.time_out_respawn_time is not None
|
||||
flag.time_out_respawn_time -= 1
|
||||
if flag.time_out_respawn_time <= 0:
|
||||
flag.handlemessage(bs.DieMessage())
|
||||
else:
|
||||
time_out_counting_down = False
|
||||
|
||||
if flag.node and flag.counter:
|
||||
pos = flag.node.position
|
||||
flag.counter.position = (pos[0], pos[1] + 1.3, pos[2])
|
||||
|
||||
# If there's no self-touches on this flag, set its text
|
||||
# to show its auto-return counter. (if there's self-touches
|
||||
# its showing that time).
|
||||
if team.flag_return_touches == 0:
|
||||
flag.counter.text = (
|
||||
str(flag.time_out_respawn_time)
|
||||
if (
|
||||
time_out_counting_down
|
||||
and flag.time_out_respawn_time is not None
|
||||
and flag.time_out_respawn_time <= 10
|
||||
)
|
||||
else ''
|
||||
)
|
||||
flag.counter.color = (1, 1, 1, 0.5)
|
||||
flag.counter.scale = 0.014
|
||||
|
||||
def _score(self, team: Team) -> None:
|
||||
team.score += 1
|
||||
self._score_sound.play()
|
||||
self._flash_base(team)
|
||||
self._update_scoreboard()
|
||||
|
||||
# Have teammates celebrate.
|
||||
for player in team.players:
|
||||
if player.actor:
|
||||
player.actor.handlemessage(bs.CelebrateMessage(2.0))
|
||||
|
||||
# Reset all flags/state.
|
||||
for reset_team in self.teams:
|
||||
if not reset_team.home_flag_at_base:
|
||||
assert reset_team.flag is not None
|
||||
reset_team.flag.handlemessage(bs.DieMessage())
|
||||
reset_team.enemy_flag_at_base = False
|
||||
if team.score >= self._score_to_win:
|
||||
self.end_game()
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = bs.GameResults()
|
||||
for team in self.teams:
|
||||
results.set_team_score(team, team.score)
|
||||
self.end(results=results, announce_delay=0.8)
|
||||
|
||||
def _handle_flag_left_base(self, team: Team) -> None:
|
||||
cur_time = bs.time()
|
||||
try:
|
||||
flag = bs.getcollision().opposingnode.getdelegate(CTFFlag, True)
|
||||
except bs.NotFoundError:
|
||||
# This can happen if the flag stops touching us due to being
|
||||
# deleted; that's ok.
|
||||
return
|
||||
|
||||
if flag.team is team:
|
||||
# Check times here to prevent too much flashing.
|
||||
if (
|
||||
team.last_flag_leave_time is None
|
||||
or cur_time - team.last_flag_leave_time > 3.0
|
||||
):
|
||||
self._alarmsound.play(position=team.base_pos)
|
||||
self._flash_base(team)
|
||||
team.last_flag_leave_time = cur_time
|
||||
team.home_flag_at_base = False
|
||||
else:
|
||||
team.enemy_flag_at_base = False
|
||||
|
||||
def _touch_return_update(self, team: Team) -> None:
|
||||
# Count down only while its away from base and not being held.
|
||||
assert team.flag is not None
|
||||
if team.home_flag_at_base or team.flag.held_count > 0:
|
||||
team.touch_return_timer_ticking = None
|
||||
return # No need to return when its at home.
|
||||
if team.touch_return_timer_ticking is None:
|
||||
team.touch_return_timer_ticking = bs.NodeActor(
|
||||
bs.newnode(
|
||||
'sound',
|
||||
attrs={
|
||||
'sound': self._ticking_sound,
|
||||
'positional': False,
|
||||
'loop': True,
|
||||
},
|
||||
)
|
||||
)
|
||||
flag = team.flag
|
||||
if flag.touch_return_time is not None:
|
||||
flag.touch_return_time -= 0.1
|
||||
if flag.counter:
|
||||
flag.counter.text = f'{flag.touch_return_time:.1f}'
|
||||
flag.counter.color = (1, 1, 0, 1)
|
||||
flag.counter.scale = 0.02
|
||||
|
||||
if flag.touch_return_time <= 0.0:
|
||||
self._award_players_touching_own_flag(team)
|
||||
flag.handlemessage(bs.DieMessage())
|
||||
|
||||
def _award_players_touching_own_flag(self, team: Team) -> None:
|
||||
for player in team.players:
|
||||
if player.touching_own_flag > 0:
|
||||
return_score = 10 + 5 * int(self.flag_touch_return_time)
|
||||
self.stats.player_scored(
|
||||
player, return_score, screenmessage=False
|
||||
)
|
||||
|
||||
def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None:
|
||||
"""Called when a player touches or stops touching their own team flag.
|
||||
|
||||
We keep track of when each player is touching their own flag so we
|
||||
can award points when returned.
|
||||
"""
|
||||
player: Player | None
|
||||
try:
|
||||
spaz = bs.getcollision().sourcenode.getdelegate(PlayerSpaz, True)
|
||||
except bs.NotFoundError:
|
||||
return
|
||||
|
||||
if not spaz.is_alive():
|
||||
return
|
||||
|
||||
player = spaz.getplayer(Player, True)
|
||||
|
||||
if player:
|
||||
player.touching_own_flag += 1 if connecting else -1
|
||||
|
||||
# If return-time is zero, just kill it immediately.. otherwise keep
|
||||
# track of touches and count down.
|
||||
if float(self.flag_touch_return_time) <= 0.0:
|
||||
assert team.flag is not None
|
||||
if (
|
||||
connecting
|
||||
and not team.home_flag_at_base
|
||||
and team.flag.held_count == 0
|
||||
):
|
||||
self._award_players_touching_own_flag(team)
|
||||
bs.getcollision().opposingnode.handlemessage(bs.DieMessage())
|
||||
|
||||
# Takes a non-zero amount of time to return.
|
||||
else:
|
||||
if connecting:
|
||||
team.flag_return_touches += 1
|
||||
if team.flag_return_touches == 1:
|
||||
team.touch_return_timer = bs.Timer(
|
||||
0.1,
|
||||
call=bs.Call(self._touch_return_update, team),
|
||||
repeat=True,
|
||||
)
|
||||
team.touch_return_timer_ticking = None
|
||||
else:
|
||||
team.flag_return_touches -= 1
|
||||
if team.flag_return_touches == 0:
|
||||
team.touch_return_timer = None
|
||||
team.touch_return_timer_ticking = None
|
||||
if team.flag_return_touches < 0:
|
||||
logging.exception('CTF flag_return_touches < 0')
|
||||
|
||||
def _flash_base(self, team: Team, length: float = 2.0) -> None:
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': team.base_pos,
|
||||
'height_attenuated': False,
|
||||
'radius': 0.3,
|
||||
'color': team.color,
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
|
||||
bs.timer(length, light.delete)
|
||||
|
||||
def spawn_player_spaz(
|
||||
self,
|
||||
player: Player,
|
||||
position: Sequence[float] | None = None,
|
||||
angle: float | None = None,
|
||||
) -> PlayerSpaz:
|
||||
"""Intercept new spazzes and add our team material for them."""
|
||||
spaz = super().spawn_player_spaz(player, position, angle)
|
||||
player = spaz.getplayer(Player, True)
|
||||
team: Team = player.team
|
||||
player.touching_own_flag = 0
|
||||
no_physical_mats: list[bs.Material] = [
|
||||
team.spaz_material_no_flag_physical
|
||||
]
|
||||
no_collide_mats: list[bs.Material] = [
|
||||
team.spaz_material_no_flag_collide
|
||||
]
|
||||
|
||||
# Our normal parts should still collide; just not physically
|
||||
# (so we can calc restores).
|
||||
assert spaz.node
|
||||
spaz.node.materials = list(spaz.node.materials) + no_physical_mats
|
||||
spaz.node.roller_materials = (
|
||||
list(spaz.node.roller_materials) + no_physical_mats
|
||||
)
|
||||
|
||||
# Pickups and punches shouldn't hit at all though.
|
||||
spaz.node.punch_materials = (
|
||||
list(spaz.node.punch_materials) + no_collide_mats
|
||||
)
|
||||
spaz.node.pickup_materials = (
|
||||
list(spaz.node.pickup_materials) + no_collide_mats
|
||||
)
|
||||
spaz.node.extras_material = (
|
||||
list(spaz.node.extras_material) + no_collide_mats
|
||||
)
|
||||
return spaz
|
||||
|
||||
def _update_scoreboard(self) -> 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, bs.PlayerDiedMessage):
|
||||
super().handlemessage(msg) # Augment standard behavior.
|
||||
self.respawn_player(msg.getplayer(Player))
|
||||
|
||||
elif isinstance(msg, FlagDiedMessage):
|
||||
assert isinstance(msg.flag, CTFFlag)
|
||||
bs.timer(0.1, bs.Call(self._spawn_flag_for_team, msg.flag.team))
|
||||
|
||||
elif isinstance(msg, FlagPickedUpMessage):
|
||||
# Store the last player to hold the flag for scoring purposes.
|
||||
assert isinstance(msg.flag, CTFFlag)
|
||||
try:
|
||||
msg.flag.last_player_to_hold = msg.node.getdelegate(
|
||||
PlayerSpaz, True
|
||||
).getplayer(Player, True)
|
||||
except bs.NotFoundError:
|
||||
pass
|
||||
|
||||
msg.flag.held_count += 1
|
||||
msg.flag.reset_return_times()
|
||||
|
||||
elif isinstance(msg, FlagDroppedMessage):
|
||||
# Store the last player to hold the flag for scoring purposes.
|
||||
assert isinstance(msg.flag, CTFFlag)
|
||||
msg.flag.held_count -= 1
|
||||
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
362
dist/ba_data/python/bascenev1lib/game/chosenone.py
vendored
Normal file
362
dist/ba_data/python/bascenev1lib/game/chosenone.py
vendored
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides the chosen-one mini-game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.flag import Flag
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.gameutils import SharedObjects
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.chosen_light: bs.NodeActor | None = None
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self, time_remaining: int) -> None:
|
||||
self.time_remaining = time_remaining
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class ChosenOneGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""
|
||||
Game involving trying to remain the one 'chosen one'
|
||||
for a set length of time while everyone else tries to
|
||||
kill you and become the chosen one themselves.
|
||||
"""
|
||||
|
||||
name = 'Chosen One'
|
||||
description = (
|
||||
'Be the chosen one for a length of time to win.\n'
|
||||
'Kill the chosen one to become it.'
|
||||
)
|
||||
available_settings = [
|
||||
bs.IntSetting(
|
||||
'Chosen One Time',
|
||||
min_value=10,
|
||||
default=30,
|
||||
increment=10,
|
||||
),
|
||||
bs.BoolSetting('Chosen One Gets Gloves', default=True),
|
||||
bs.BoolSetting('Chosen One Gets Shield', default=False),
|
||||
bs.IntChoiceSetting(
|
||||
'Time Limit',
|
||||
choices=[
|
||||
('None', 0),
|
||||
('1 Minute', 60),
|
||||
('2 Minutes', 120),
|
||||
('5 Minutes', 300),
|
||||
('10 Minutes', 600),
|
||||
('20 Minutes', 1200),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
bs.FloatChoiceSetting(
|
||||
'Respawn Times',
|
||||
choices=[
|
||||
('Shorter', 0.25),
|
||||
('Short', 0.5),
|
||||
('Normal', 1.0),
|
||||
('Long', 2.0),
|
||||
('Longer', 4.0),
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
bs.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
scoreconfig = bs.ScoreConfig(label='Time Held')
|
||||
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
assert bs.app.classic is not None
|
||||
return bs.app.classic.getmaps('keep_away')
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
self._scoreboard = Scoreboard()
|
||||
self._chosen_one_player: Player | None = None
|
||||
self._swipsound = bs.getsound('swip')
|
||||
self._countdownsounds: dict[int, bs.Sound] = {
|
||||
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'),
|
||||
}
|
||||
self._flag_spawn_pos: Sequence[float] | None = None
|
||||
self._reset_region_material: bs.Material | None = None
|
||||
self._flag: Flag | None = None
|
||||
self._reset_region: bs.Node | None = None
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self._chosen_one_time = int(settings['Chosen One Time'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
self._chosen_one_gets_shield = bool(settings['Chosen One Gets Shield'])
|
||||
self._chosen_one_gets_gloves = bool(settings['Chosen One Gets Gloves'])
|
||||
|
||||
# Base class overrides
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.CHOSEN_ONE
|
||||
)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
return 'There can be only one.'
|
||||
|
||||
def create_team(self, sessionteam: bs.SessionTeam) -> Team:
|
||||
return Team(time_remaining=self._chosen_one_time)
|
||||
|
||||
def on_team_join(self, team: Team) -> None:
|
||||
self._update_scoreboard()
|
||||
|
||||
def on_player_leave(self, player: Player) -> None:
|
||||
super().on_player_leave(player)
|
||||
if self._get_chosen_one_player() is player:
|
||||
self._set_chosen_one_player(None)
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
shared = SharedObjects.get()
|
||||
self.setup_standard_time_limit(self._time_limit)
|
||||
self.setup_standard_powerup_drops()
|
||||
self._flag_spawn_pos = self.map.get_flag_position(None)
|
||||
Flag.project_stand(self._flag_spawn_pos)
|
||||
bs.timer(1.0, call=self._tick, repeat=True)
|
||||
|
||||
mat = self._reset_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', bs.WeakCall(self._handle_reset_collide)),
|
||||
),
|
||||
)
|
||||
|
||||
self._set_chosen_one_player(None)
|
||||
|
||||
def _create_reset_region(self) -> None:
|
||||
assert self._reset_region_material is not None
|
||||
assert self._flag_spawn_pos is not None
|
||||
pos = self._flag_spawn_pos
|
||||
self._reset_region = bs.newnode(
|
||||
'region',
|
||||
attrs={
|
||||
'position': (pos[0], pos[1] + 0.75, pos[2]),
|
||||
'scale': (0.5, 0.5, 0.5),
|
||||
'type': 'sphere',
|
||||
'materials': [self._reset_region_material],
|
||||
},
|
||||
)
|
||||
|
||||
def _get_chosen_one_player(self) -> Player | None:
|
||||
# Should never return invalid references; return None in that case.
|
||||
if self._chosen_one_player:
|
||||
return self._chosen_one_player
|
||||
return None
|
||||
|
||||
def _handle_reset_collide(self) -> None:
|
||||
# If we have a chosen one, ignore these.
|
||||
if self._get_chosen_one_player() is not None:
|
||||
return
|
||||
|
||||
# Attempt to get a Actor that we hit.
|
||||
try:
|
||||
spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
|
||||
player = spaz.getplayer(Player, True)
|
||||
except bs.NotFoundError:
|
||||
return
|
||||
|
||||
if spaz.is_alive():
|
||||
self._set_chosen_one_player(player)
|
||||
|
||||
def _flash_flag_spawn(self) -> None:
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': self._flag_spawn_pos,
|
||||
'color': (1, 1, 1),
|
||||
'radius': 0.3,
|
||||
'height_attenuated': False,
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True)
|
||||
bs.timer(1.0, light.delete)
|
||||
|
||||
def _tick(self) -> None:
|
||||
# Give the chosen one points.
|
||||
player = self._get_chosen_one_player()
|
||||
if player is not None:
|
||||
# This shouldn't happen, but just in case.
|
||||
if not player.is_alive():
|
||||
logging.error('got dead player as chosen one in _tick')
|
||||
self._set_chosen_one_player(None)
|
||||
else:
|
||||
scoring_team = player.team
|
||||
self.stats.player_scored(
|
||||
player, 3, screenmessage=False, display=False
|
||||
)
|
||||
|
||||
scoring_team.time_remaining = max(
|
||||
0, scoring_team.time_remaining - 1
|
||||
)
|
||||
|
||||
# Show the count over their head
|
||||
if scoring_team.time_remaining > 0:
|
||||
if isinstance(player.actor, PlayerSpaz) and player.actor:
|
||||
player.actor.set_score_text(
|
||||
str(scoring_team.time_remaining)
|
||||
)
|
||||
|
||||
self._update_scoreboard()
|
||||
|
||||
# announce numbers we have sounds for
|
||||
if scoring_team.time_remaining in self._countdownsounds:
|
||||
self._countdownsounds[scoring_team.time_remaining].play()
|
||||
|
||||
# Winner!
|
||||
if scoring_team.time_remaining <= 0:
|
||||
self.end_game()
|
||||
|
||||
else:
|
||||
# (player is None)
|
||||
# This shouldn't happen, but just in case.
|
||||
# (Chosen-one player ceasing to exist should
|
||||
# trigger on_player_leave which resets chosen-one)
|
||||
if self._chosen_one_player is not None:
|
||||
logging.error('got nonexistent player as chosen one in _tick')
|
||||
self._set_chosen_one_player(None)
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = bs.GameResults()
|
||||
for team in self.teams:
|
||||
results.set_team_score(
|
||||
team, self._chosen_one_time - team.time_remaining
|
||||
)
|
||||
self.end(results=results, announce_delay=0)
|
||||
|
||||
def _set_chosen_one_player(self, player: Player | None) -> None:
|
||||
existing = self._get_chosen_one_player()
|
||||
if existing:
|
||||
existing.chosen_light = None
|
||||
self._swipsound.play()
|
||||
if not player:
|
||||
assert self._flag_spawn_pos is not None
|
||||
self._flag = Flag(
|
||||
color=(1, 0.9, 0.2),
|
||||
position=self._flag_spawn_pos,
|
||||
touchable=False,
|
||||
)
|
||||
self._chosen_one_player = None
|
||||
|
||||
# Create a light to highlight the flag;
|
||||
# this will go away when the flag dies.
|
||||
bs.newnode(
|
||||
'light',
|
||||
owner=self._flag.node,
|
||||
attrs={
|
||||
'position': self._flag_spawn_pos,
|
||||
'intensity': 0.6,
|
||||
'height_attenuated': False,
|
||||
'volume_intensity_scale': 0.1,
|
||||
'radius': 0.1,
|
||||
'color': (1.2, 1.2, 0.4),
|
||||
},
|
||||
)
|
||||
|
||||
# Also an extra momentary flash.
|
||||
self._flash_flag_spawn()
|
||||
|
||||
# Re-create our flag region in case if someone is waiting for
|
||||
# flag right there:
|
||||
self._create_reset_region()
|
||||
else:
|
||||
if player.actor:
|
||||
self._flag = None
|
||||
self._chosen_one_player = player
|
||||
|
||||
if self._chosen_one_gets_shield:
|
||||
player.actor.handlemessage(bs.PowerupMessage('shield'))
|
||||
if self._chosen_one_gets_gloves:
|
||||
player.actor.handlemessage(bs.PowerupMessage('punch'))
|
||||
|
||||
# Use a color that's partway between their team color
|
||||
# and white.
|
||||
color = [
|
||||
0.3 + c * 0.7
|
||||
for c in bs.normalized_color(player.team.color)
|
||||
]
|
||||
light = player.chosen_light = bs.NodeActor(
|
||||
bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'intensity': 0.6,
|
||||
'height_attenuated': False,
|
||||
'volume_intensity_scale': 0.1,
|
||||
'radius': 0.13,
|
||||
'color': color,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert light.node
|
||||
bs.animate(
|
||||
light.node,
|
||||
'intensity',
|
||||
{0: 1.0, 0.2: 0.4, 0.4: 1.0},
|
||||
loop=True,
|
||||
)
|
||||
assert isinstance(player.actor, PlayerSpaz)
|
||||
player.actor.node.connectattr(
|
||||
'position', light.node, 'position'
|
||||
)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
player = msg.getplayer(Player)
|
||||
if player is self._get_chosen_one_player():
|
||||
killerplayer = msg.getkillerplayer(Player)
|
||||
self._set_chosen_one_player(
|
||||
None
|
||||
if (
|
||||
killerplayer is None
|
||||
or killerplayer is player
|
||||
or not killerplayer.is_alive()
|
||||
)
|
||||
else killerplayer
|
||||
)
|
||||
self.respawn_player(player)
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
|
||||
def _update_scoreboard(self) -> None:
|
||||
for team in self.teams:
|
||||
self._scoreboard.set_team_value(
|
||||
team, team.time_remaining, self._chosen_one_time, countdown=True
|
||||
)
|
||||
330
dist/ba_data/python/bascenev1lib/game/conquest.py
vendored
Normal file
330
dist/ba_data/python/bascenev1lib/game/conquest.py
vendored
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides the Conquest game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.flag import Flag
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.gameutils import SharedObjects
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
from bascenev1lib.actor.respawnicon import RespawnIcon
|
||||
|
||||
|
||||
class ConquestFlag(Flag):
|
||||
"""A custom flag for use with Conquest games."""
|
||||
|
||||
def __init__(self, *args: Any, **keywds: Any):
|
||||
super().__init__(*args, **keywds)
|
||||
self._team: Team | None = None
|
||||
self.light: bs.Node | None = None
|
||||
|
||||
@property
|
||||
def team(self) -> Team | None:
|
||||
"""The team that owns this flag."""
|
||||
return self._team
|
||||
|
||||
@team.setter
|
||||
def team(self, team: Team) -> None:
|
||||
"""Set the team that owns this flag."""
|
||||
self._team = team
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
# FIXME: We shouldn't be using customdata here
|
||||
# (but need to update respawn funcs accordingly first).
|
||||
@property
|
||||
def respawn_timer(self) -> bs.Timer | None:
|
||||
"""Type safe access to standard respawn timer."""
|
||||
return self.customdata.get('respawn_timer', None)
|
||||
|
||||
@respawn_timer.setter
|
||||
def respawn_timer(self, value: bs.Timer | None) -> None:
|
||||
self.customdata['respawn_timer'] = value
|
||||
|
||||
@property
|
||||
def respawn_icon(self) -> RespawnIcon | None:
|
||||
"""Type safe access to standard respawn icon."""
|
||||
return self.customdata.get('respawn_icon', None)
|
||||
|
||||
@respawn_icon.setter
|
||||
def respawn_icon(self, value: RespawnIcon | None) -> None:
|
||||
self.customdata['respawn_icon'] = value
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.flags_held = 0
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class ConquestGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""A game where teams try to claim all flags on the map."""
|
||||
|
||||
name = 'Conquest'
|
||||
description = 'Secure all flags on the map to win.'
|
||||
available_settings = [
|
||||
bs.IntChoiceSetting(
|
||||
'Time Limit',
|
||||
choices=[
|
||||
('None', 0),
|
||||
('1 Minute', 60),
|
||||
('2 Minutes', 120),
|
||||
('5 Minutes', 300),
|
||||
('10 Minutes', 600),
|
||||
('20 Minutes', 1200),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
bs.FloatChoiceSetting(
|
||||
'Respawn Times',
|
||||
choices=[
|
||||
('Shorter', 0.25),
|
||||
('Short', 0.5),
|
||||
('Normal', 1.0),
|
||||
('Long', 2.0),
|
||||
('Longer', 4.0),
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
bs.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
return issubclass(sessiontype, bs.DualTeamSession)
|
||||
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
assert bs.app.classic is not None
|
||||
return bs.app.classic.getmaps('conquest')
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
shared = SharedObjects.get()
|
||||
self._scoreboard = Scoreboard()
|
||||
self._score_sound = bs.getsound('score')
|
||||
self._swipsound = bs.getsound('swip')
|
||||
self._extraflagmat = bs.Material()
|
||||
self._flags: list[ConquestFlag] = []
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
|
||||
# Base class overrides.
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.GRAND_ROMP
|
||||
)
|
||||
|
||||
# We want flags to tell us they've been hit but not react physically.
|
||||
self._extraflagmat.add_actions(
|
||||
conditions=('they_have_material', shared.player_material),
|
||||
actions=(
|
||||
('modify_part_collision', 'collide', True),
|
||||
('call', 'at_connect', self._handle_flag_player_collide),
|
||||
),
|
||||
)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
return 'Secure all ${ARG1} flags.', len(self.map.flag_points)
|
||||
|
||||
def get_instance_description_short(self) -> str | Sequence:
|
||||
return 'secure all ${ARG1} flags', len(self.map.flag_points)
|
||||
|
||||
def on_team_join(self, team: Team) -> None:
|
||||
if self.has_begun():
|
||||
self._update_scores()
|
||||
|
||||
def on_player_join(self, player: Player) -> None:
|
||||
player.respawn_timer = None
|
||||
|
||||
# Only spawn if this player's team has a flag currently.
|
||||
if player.team.flags_held > 0:
|
||||
self.spawn_player(player)
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
self.setup_standard_time_limit(self._time_limit)
|
||||
self.setup_standard_powerup_drops()
|
||||
|
||||
# Set up flags with marker lights.
|
||||
for i, flag_point in enumerate(self.map.flag_points):
|
||||
point = flag_point
|
||||
flag = ConquestFlag(
|
||||
position=point, touchable=False, materials=[self._extraflagmat]
|
||||
)
|
||||
self._flags.append(flag)
|
||||
Flag.project_stand(point)
|
||||
flag.light = bs.newnode(
|
||||
'light',
|
||||
owner=flag.node,
|
||||
attrs={
|
||||
'position': point,
|
||||
'intensity': 0.25,
|
||||
'height_attenuated': False,
|
||||
'radius': 0.3,
|
||||
'color': (1, 1, 1),
|
||||
},
|
||||
)
|
||||
|
||||
# Give teams a flag to start with.
|
||||
for i, team in enumerate(self.teams):
|
||||
self._flags[i].team = team
|
||||
light = self._flags[i].light
|
||||
assert light
|
||||
node = self._flags[i].node
|
||||
assert node
|
||||
light.color = team.color
|
||||
node.color = team.color
|
||||
|
||||
self._update_scores()
|
||||
|
||||
# Initial joiners didn't spawn due to no flags being owned yet;
|
||||
# spawn them now.
|
||||
for player in self.players:
|
||||
self.spawn_player(player)
|
||||
|
||||
def _update_scores(self) -> None:
|
||||
for team in self.teams:
|
||||
team.flags_held = 0
|
||||
for flag in self._flags:
|
||||
if flag.team is not None:
|
||||
flag.team.flags_held += 1
|
||||
for team in self.teams:
|
||||
# If a team finds themselves with no flags, cancel all
|
||||
# outstanding spawn-timers.
|
||||
if team.flags_held == 0:
|
||||
for player in team.players:
|
||||
player.respawn_timer = None
|
||||
player.respawn_icon = None
|
||||
if team.flags_held == len(self._flags):
|
||||
self.end_game()
|
||||
self._scoreboard.set_team_value(
|
||||
team, team.flags_held, len(self._flags)
|
||||
)
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = bs.GameResults()
|
||||
for team in self.teams:
|
||||
results.set_team_score(team, team.flags_held)
|
||||
self.end(results=results)
|
||||
|
||||
def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None:
|
||||
assert flag.node
|
||||
assert flag.light
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': flag.node.position,
|
||||
'height_attenuated': False,
|
||||
'color': flag.light.color,
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True)
|
||||
bs.timer(length, light.delete)
|
||||
|
||||
def _handle_flag_player_collide(self) -> None:
|
||||
collision = bs.getcollision()
|
||||
try:
|
||||
flag = collision.sourcenode.getdelegate(ConquestFlag, True)
|
||||
player = collision.opposingnode.getdelegate(
|
||||
PlayerSpaz, True
|
||||
).getplayer(Player, True)
|
||||
except bs.NotFoundError:
|
||||
return
|
||||
assert flag.light
|
||||
|
||||
if flag.team is not player.team:
|
||||
flag.team = player.team
|
||||
flag.light.color = player.team.color
|
||||
flag.node.color = player.team.color
|
||||
self.stats.player_scored(player, 10, screenmessage=False)
|
||||
self._swipsound.play()
|
||||
self._flash_flag(flag)
|
||||
self._update_scores()
|
||||
|
||||
# Respawn any players on this team that were in limbo due to the
|
||||
# lack of a flag for their team.
|
||||
for otherplayer in self.players:
|
||||
if (
|
||||
otherplayer.team is flag.team
|
||||
and otherplayer.actor is not None
|
||||
and not otherplayer.is_alive()
|
||||
and otherplayer.respawn_timer is None
|
||||
):
|
||||
self.spawn_player(otherplayer)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
|
||||
# Respawn only if this team has a flag.
|
||||
player = msg.getplayer(Player)
|
||||
if player.team.flags_held > 0:
|
||||
self.respawn_player(player)
|
||||
else:
|
||||
player.respawn_timer = None
|
||||
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
|
||||
def spawn_player(self, player: Player) -> bs.Actor:
|
||||
# We spawn players at different places based on what flags are held.
|
||||
return self.spawn_player_spaz(
|
||||
player, self._get_player_spawn_position(player)
|
||||
)
|
||||
|
||||
def _get_player_spawn_position(self, player: Player) -> Sequence[float]:
|
||||
# Iterate until we find a spawn owned by this team.
|
||||
spawn_count = len(self.map.spawn_by_flag_points)
|
||||
|
||||
# Get all spawns owned by this team.
|
||||
spawns = [
|
||||
i for i in range(spawn_count) if self._flags[i].team is player.team
|
||||
]
|
||||
|
||||
closest_spawn = 0
|
||||
closest_distance = 9999.0
|
||||
|
||||
# Now find the spawn that's closest to a spawn not owned by us;
|
||||
# we'll use that one.
|
||||
for spawn in spawns:
|
||||
spt = self.map.spawn_by_flag_points[spawn]
|
||||
our_pt = bs.Vec3(spt[0], spt[1], spt[2])
|
||||
for otherspawn in [
|
||||
i
|
||||
for i in range(spawn_count)
|
||||
if self._flags[i].team is not player.team
|
||||
]:
|
||||
spt = self.map.spawn_by_flag_points[otherspawn]
|
||||
their_pt = bs.Vec3(spt[0], spt[1], spt[2])
|
||||
dist = (their_pt - our_pt).length()
|
||||
if dist < closest_distance:
|
||||
closest_distance = dist
|
||||
closest_spawn = spawn
|
||||
|
||||
pos = self.map.spawn_by_flag_points[closest_spawn]
|
||||
x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3])
|
||||
z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5])
|
||||
pos = (
|
||||
pos[0] + random.uniform(*x_range),
|
||||
pos[1],
|
||||
pos[2] + random.uniform(*z_range),
|
||||
)
|
||||
return pos
|
||||
204
dist/ba_data/python/bascenev1lib/game/deathmatch.py
vendored
Normal file
204
dist/ba_data/python/bascenev1lib/game/deathmatch.py
vendored
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""DeathMatch game and support classes."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.score = 0
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class DeathMatchGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""A game type based on acquiring kills."""
|
||||
|
||||
name = 'Death Match'
|
||||
description = 'Kill a set number of enemies to win.'
|
||||
|
||||
# Print messages when players die since it matters here.
|
||||
announce_player_deaths = True
|
||||
|
||||
@classmethod
|
||||
def get_available_settings(
|
||||
cls, sessiontype: type[bs.Session]
|
||||
) -> list[bs.Setting]:
|
||||
settings = [
|
||||
bs.IntSetting(
|
||||
'Kills to Win Per Player',
|
||||
min_value=1,
|
||||
default=5,
|
||||
increment=1,
|
||||
),
|
||||
bs.IntChoiceSetting(
|
||||
'Time Limit',
|
||||
choices=[
|
||||
('None', 0),
|
||||
('1 Minute', 60),
|
||||
('2 Minutes', 120),
|
||||
('5 Minutes', 300),
|
||||
('10 Minutes', 600),
|
||||
('20 Minutes', 1200),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
bs.FloatChoiceSetting(
|
||||
'Respawn Times',
|
||||
choices=[
|
||||
('Shorter', 0.25),
|
||||
('Short', 0.5),
|
||||
('Normal', 1.0),
|
||||
('Long', 2.0),
|
||||
('Longer', 4.0),
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
bs.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
|
||||
# In teams mode, a suicide gives a point to the other team, but in
|
||||
# free-for-all it subtracts from your own score. By default we clamp
|
||||
# this at zero to benefit new players, but pro players might like to
|
||||
# be able to go negative. (to avoid a strategy of just
|
||||
# suiciding until you get a good drop)
|
||||
if issubclass(sessiontype, bs.FreeForAllSession):
|
||||
settings.append(
|
||||
bs.BoolSetting('Allow Negative Scores', default=False)
|
||||
)
|
||||
|
||||
return settings
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
return issubclass(sessiontype, bs.DualTeamSession) or issubclass(
|
||||
sessiontype, bs.FreeForAllSession
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
assert bs.app.classic is not None
|
||||
return bs.app.classic.getmaps('melee')
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
self._scoreboard = Scoreboard()
|
||||
self._score_to_win: int | None = None
|
||||
self._dingsound = bs.getsound('dingSmall')
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self._kills_to_win_per_player = int(settings['Kills to Win Per Player'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
self._allow_negative_scores = bool(
|
||||
settings.get('Allow Negative Scores', False)
|
||||
)
|
||||
|
||||
# Base class overrides.
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.TO_THE_DEATH
|
||||
)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
return 'Crush ${ARG1} of your enemies.', self._score_to_win
|
||||
|
||||
def get_instance_description_short(self) -> str | Sequence:
|
||||
return 'kill ${ARG1} enemies', self._score_to_win
|
||||
|
||||
def on_team_join(self, team: Team) -> None:
|
||||
if self.has_begun():
|
||||
self._update_scoreboard()
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
self.setup_standard_time_limit(self._time_limit)
|
||||
self.setup_standard_powerup_drops()
|
||||
|
||||
# Base kills needed to win on the size of the largest team.
|
||||
self._score_to_win = self._kills_to_win_per_player * max(
|
||||
1, max((len(t.players) for t in self.teams), default=0)
|
||||
)
|
||||
self._update_scoreboard()
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
|
||||
player = msg.getplayer(Player)
|
||||
self.respawn_player(player)
|
||||
|
||||
killer = msg.getkillerplayer(Player)
|
||||
if killer is None:
|
||||
return None
|
||||
|
||||
# Handle team-kills.
|
||||
if killer.team is player.team:
|
||||
# In free-for-all, killing yourself loses you a point.
|
||||
if isinstance(self.session, bs.FreeForAllSession):
|
||||
new_score = player.team.score - 1
|
||||
if not self._allow_negative_scores:
|
||||
new_score = max(0, new_score)
|
||||
player.team.score = new_score
|
||||
|
||||
# In teams-mode it gives a point to the other team.
|
||||
else:
|
||||
self._dingsound.play()
|
||||
for team in self.teams:
|
||||
if team is not killer.team:
|
||||
team.score += 1
|
||||
|
||||
# Killing someone on another team nets a kill.
|
||||
else:
|
||||
killer.team.score += 1
|
||||
self._dingsound.play()
|
||||
|
||||
# In FFA show scores since its hard to find on the scoreboard.
|
||||
if isinstance(killer.actor, PlayerSpaz) and killer.actor:
|
||||
killer.actor.set_score_text(
|
||||
str(killer.team.score) + '/' + str(self._score_to_win),
|
||||
color=killer.team.color,
|
||||
flash=True,
|
||||
)
|
||||
|
||||
self._update_scoreboard()
|
||||
|
||||
# If someone has won, set a timer to end shortly.
|
||||
# (allows the dust to clear and draws to occur if deaths are
|
||||
# close enough)
|
||||
assert self._score_to_win is not None
|
||||
if any(team.score >= self._score_to_win for team in self.teams):
|
||||
bs.timer(0.5, self.end_game)
|
||||
|
||||
else:
|
||||
return super().handlemessage(msg)
|
||||
return None
|
||||
|
||||
def _update_scoreboard(self) -> None:
|
||||
for team in self.teams:
|
||||
self._scoreboard.set_team_value(
|
||||
team, team.score, self._score_to_win
|
||||
)
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = bs.GameResults()
|
||||
for team in self.teams:
|
||||
results.set_team_score(team, team.score)
|
||||
self.end(results=results)
|
||||
301
dist/ba_data/python/bascenev1lib/game/easteregghunt.py
vendored
Normal file
301
dist/ba_data/python/bascenev1lib/game/easteregghunt.py
vendored
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides an easter egg hunt game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.bomb import Bomb
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.actor.spazbot import SpazBotSet, BouncyBot, SpazBotDiedMessage
|
||||
from bascenev1lib.actor.onscreencountdown import OnScreenCountdown
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.actor.respawnicon import RespawnIcon
|
||||
from bascenev1lib.gameutils import SharedObjects
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.respawn_timer: bs.Timer | None = None
|
||||
self.respawn_icon: RespawnIcon | None = None
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.score = 0
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class EasterEggHuntGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""A game where score is based on collecting eggs."""
|
||||
|
||||
name = 'Easter Egg Hunt'
|
||||
description = 'Gather eggs!'
|
||||
available_settings = [
|
||||
bs.BoolSetting('Pro Mode', default=False),
|
||||
bs.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
scoreconfig = bs.ScoreConfig(label='Score', scoretype=bs.ScoreType.POINTS)
|
||||
|
||||
# We're currently hard-coded for one map.
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
return ['Tower D']
|
||||
|
||||
# We support teams, free-for-all, and co-op sessions.
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
return (
|
||||
issubclass(sessiontype, bs.CoopSession)
|
||||
or issubclass(sessiontype, bs.DualTeamSession)
|
||||
or issubclass(sessiontype, bs.FreeForAllSession)
|
||||
)
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
shared = SharedObjects.get()
|
||||
self._last_player_death_time = None
|
||||
self._scoreboard = Scoreboard()
|
||||
self.egg_mesh = bs.getmesh('egg')
|
||||
self.egg_tex_1 = bs.gettexture('eggTex1')
|
||||
self.egg_tex_2 = bs.gettexture('eggTex2')
|
||||
self.egg_tex_3 = bs.gettexture('eggTex3')
|
||||
self._collect_sound = bs.getsound('powerup01')
|
||||
self._pro_mode = settings.get('Pro Mode', False)
|
||||
self._epic_mode = settings.get('Epic Mode', False)
|
||||
self._max_eggs = 1.0
|
||||
self.egg_material = bs.Material()
|
||||
self.egg_material.add_actions(
|
||||
conditions=('they_have_material', shared.player_material),
|
||||
actions=(('call', 'at_connect', self._on_egg_player_collide),),
|
||||
)
|
||||
self._eggs: list[Egg] = []
|
||||
self._update_timer: bs.Timer | None = None
|
||||
self._countdown: OnScreenCountdown | None = None
|
||||
self._bots: SpazBotSet | None = None
|
||||
|
||||
# Base class overrides
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH
|
||||
)
|
||||
|
||||
def on_team_join(self, team: Team) -> None:
|
||||
if self.has_begun():
|
||||
self._update_scoreboard()
|
||||
|
||||
# Called when our game actually starts.
|
||||
def on_begin(self) -> None:
|
||||
from bascenev1lib.maps import TowerD
|
||||
|
||||
# There's a player-wall on the tower-d level to prevent
|
||||
# players from getting up on the stairs.. we wanna kill that.
|
||||
gamemap = self.map
|
||||
assert isinstance(gamemap, TowerD)
|
||||
gamemap.player_wall.delete()
|
||||
super().on_begin()
|
||||
self._update_scoreboard()
|
||||
self._update_timer = bs.Timer(0.25, self._update, repeat=True)
|
||||
self._countdown = OnScreenCountdown(60, endcall=self.end_game)
|
||||
bs.timer(4.0, self._countdown.start)
|
||||
self._bots = SpazBotSet()
|
||||
|
||||
# Spawn evil bunny in co-op only.
|
||||
if isinstance(self.session, bs.CoopSession) and self._pro_mode:
|
||||
self._spawn_evil_bunny()
|
||||
|
||||
# Overriding the default character spawning.
|
||||
def spawn_player(self, player: Player) -> bs.Actor:
|
||||
spaz = self.spawn_player_spaz(player)
|
||||
spaz.connect_controls_to_player()
|
||||
return spaz
|
||||
|
||||
def _spawn_evil_bunny(self) -> None:
|
||||
assert self._bots is not None
|
||||
self._bots.spawn_bot(BouncyBot, pos=(6, 4, -7.8), spawn_time=10.0)
|
||||
|
||||
def _on_egg_player_collide(self) -> None:
|
||||
if self.has_ended():
|
||||
return
|
||||
collision = bs.getcollision()
|
||||
|
||||
# Be defensive here; we could be hitting the corpse of a player
|
||||
# who just left/etc.
|
||||
try:
|
||||
egg = collision.sourcenode.getdelegate(Egg, True)
|
||||
player = collision.opposingnode.getdelegate(
|
||||
PlayerSpaz, True
|
||||
).getplayer(Player, True)
|
||||
except bs.NotFoundError:
|
||||
return
|
||||
|
||||
player.team.score += 1
|
||||
|
||||
# Displays a +1 (and adds to individual player score in
|
||||
# teams mode).
|
||||
self.stats.player_scored(player, 1, screenmessage=False)
|
||||
if self._max_eggs < 5:
|
||||
self._max_eggs += 1.0
|
||||
elif self._max_eggs < 10:
|
||||
self._max_eggs += 0.5
|
||||
elif self._max_eggs < 30:
|
||||
self._max_eggs += 0.3
|
||||
self._update_scoreboard()
|
||||
self._collect_sound.play(0.5, position=egg.node.position)
|
||||
|
||||
# Create a flash.
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': egg.node.position,
|
||||
'height_attenuated': False,
|
||||
'radius': 0.1,
|
||||
'color': (1, 1, 0),
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.2: 0}, loop=False)
|
||||
bs.timer(0.200, light.delete)
|
||||
egg.handlemessage(bs.DieMessage())
|
||||
|
||||
def _update(self) -> None:
|
||||
# Misc. periodic updating.
|
||||
xpos = random.uniform(-7.1, 6.0)
|
||||
ypos = random.uniform(3.5, 3.5)
|
||||
zpos = random.uniform(-8.2, 3.7)
|
||||
|
||||
# Prune dead eggs from our list.
|
||||
self._eggs = [e for e in self._eggs if e]
|
||||
|
||||
# Spawn more eggs if we've got space.
|
||||
if len(self._eggs) < int(self._max_eggs):
|
||||
# Occasionally spawn a land-mine in addition.
|
||||
if self._pro_mode and random.random() < 0.25:
|
||||
mine = Bomb(
|
||||
position=(xpos, ypos, zpos), bomb_type='land_mine'
|
||||
).autoretain()
|
||||
mine.arm()
|
||||
else:
|
||||
self._eggs.append(Egg(position=(xpos, ypos, zpos)))
|
||||
|
||||
# Various high-level game events come through this method.
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
# Respawn dead players.
|
||||
if isinstance(msg, bs.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 = bs.Timer(
|
||||
respawn_time, bs.Call(self.spawn_player_if_exists, player)
|
||||
)
|
||||
player.respawn_icon = RespawnIcon(player, respawn_time)
|
||||
|
||||
# Whenever our evil bunny dies, respawn him and spew some eggs.
|
||||
elif isinstance(msg, SpazBotDiedMessage):
|
||||
self._spawn_evil_bunny()
|
||||
assert msg.spazbot.node
|
||||
pos = msg.spazbot.node.position
|
||||
for _i in range(6):
|
||||
spread = 0.4
|
||||
self._eggs.append(
|
||||
Egg(
|
||||
position=(
|
||||
pos[0] + random.uniform(-spread, spread),
|
||||
pos[1] + random.uniform(-spread, spread),
|
||||
pos[2] + random.uniform(-spread, spread),
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Default handler.
|
||||
return super().handlemessage(msg)
|
||||
return None
|
||||
|
||||
def _update_scoreboard(self) -> None:
|
||||
for team in self.teams:
|
||||
self._scoreboard.set_team_value(team, team.score)
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = bs.GameResults()
|
||||
for team in self.teams:
|
||||
results.set_team_score(team, team.score)
|
||||
self.end(results)
|
||||
|
||||
|
||||
class Egg(bs.Actor):
|
||||
"""A lovely egg that can be picked up for points."""
|
||||
|
||||
def __init__(self, position: tuple[float, float, float] = (0.0, 1.0, 0.0)):
|
||||
super().__init__()
|
||||
activity = self.activity
|
||||
assert isinstance(activity, EasterEggHuntGame)
|
||||
shared = SharedObjects.get()
|
||||
|
||||
# Spawn just above the provided point.
|
||||
self._spawn_pos = (position[0], position[1] + 1.0, position[2])
|
||||
ctex = (activity.egg_tex_1, activity.egg_tex_2, activity.egg_tex_3)[
|
||||
random.randrange(3)
|
||||
]
|
||||
mats = [shared.object_material, activity.egg_material]
|
||||
self.node = bs.newnode(
|
||||
'prop',
|
||||
delegate=self,
|
||||
attrs={
|
||||
'mesh': activity.egg_mesh,
|
||||
'color_texture': ctex,
|
||||
'body': 'capsule',
|
||||
'reflection': 'soft',
|
||||
'mesh_scale': 0.5,
|
||||
'body_scale': 0.6,
|
||||
'density': 4.0,
|
||||
'reflection_scale': [0.15],
|
||||
'shadow_size': 0.6,
|
||||
'position': self._spawn_pos,
|
||||
'materials': mats,
|
||||
},
|
||||
)
|
||||
|
||||
def exists(self) -> bool:
|
||||
return bool(self.node)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.DieMessage):
|
||||
if self.node:
|
||||
self.node.delete()
|
||||
elif isinstance(msg, bs.HitMessage):
|
||||
if self.node:
|
||||
assert msg.force_direction is not None
|
||||
self.node.handlemessage(
|
||||
'impulse',
|
||||
msg.pos[0],
|
||||
msg.pos[1],
|
||||
msg.pos[2],
|
||||
msg.velocity[0],
|
||||
msg.velocity[1],
|
||||
msg.velocity[2],
|
||||
1.0 * msg.magnitude,
|
||||
1.0 * msg.velocity_magnitude,
|
||||
msg.radius,
|
||||
0,
|
||||
msg.force_direction[0],
|
||||
msg.force_direction[1],
|
||||
msg.force_direction[2],
|
||||
)
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
598
dist/ba_data/python/bascenev1lib/game/elimination.py
vendored
Normal file
598
dist/ba_data/python/bascenev1lib/game/elimination.py
vendored
Normal file
|
|
@ -0,0 +1,598 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Elimination mini-game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.spazfactory import SpazFactory
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class Icon(bs.Actor):
|
||||
"""Creates in in-game icon on screen."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
player: Player,
|
||||
position: tuple[float, float],
|
||||
scale: float,
|
||||
show_lives: bool = True,
|
||||
show_death: bool = True,
|
||||
name_scale: float = 1.0,
|
||||
name_maxwidth: float = 115.0,
|
||||
flatness: float = 1.0,
|
||||
shadow: float = 1.0,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self._player = player
|
||||
self._show_lives = show_lives
|
||||
self._show_death = show_death
|
||||
self._name_scale = name_scale
|
||||
self._outline_tex = bs.gettexture('characterIconMask')
|
||||
|
||||
icon = player.get_icon()
|
||||
self.node = bs.newnode(
|
||||
'image',
|
||||
delegate=self,
|
||||
attrs={
|
||||
'texture': icon['texture'],
|
||||
'tint_texture': icon['tint_texture'],
|
||||
'tint_color': icon['tint_color'],
|
||||
'vr_depth': 400,
|
||||
'tint2_color': icon['tint2_color'],
|
||||
'mask_texture': self._outline_tex,
|
||||
'opacity': 1.0,
|
||||
'absolute_scale': True,
|
||||
'attach': 'bottomCenter',
|
||||
},
|
||||
)
|
||||
self._name_text = bs.newnode(
|
||||
'text',
|
||||
owner=self.node,
|
||||
attrs={
|
||||
'text': bs.Lstr(value=player.getname()),
|
||||
'color': bs.safecolor(player.team.color),
|
||||
'h_align': 'center',
|
||||
'v_align': 'center',
|
||||
'vr_depth': 410,
|
||||
'maxwidth': name_maxwidth,
|
||||
'shadow': shadow,
|
||||
'flatness': flatness,
|
||||
'h_attach': 'center',
|
||||
'v_attach': 'bottom',
|
||||
},
|
||||
)
|
||||
if self._show_lives:
|
||||
self._lives_text = bs.newnode(
|
||||
'text',
|
||||
owner=self.node,
|
||||
attrs={
|
||||
'text': 'x0',
|
||||
'color': (1, 1, 0.5),
|
||||
'h_align': 'left',
|
||||
'vr_depth': 430,
|
||||
'shadow': 1.0,
|
||||
'flatness': 1.0,
|
||||
'h_attach': 'center',
|
||||
'v_attach': 'bottom',
|
||||
},
|
||||
)
|
||||
self.set_position_and_scale(position, scale)
|
||||
|
||||
def set_position_and_scale(
|
||||
self, position: tuple[float, float], scale: float
|
||||
) -> None:
|
||||
"""(Re)position the icon."""
|
||||
assert self.node
|
||||
self.node.position = position
|
||||
self.node.scale = [70.0 * scale]
|
||||
self._name_text.position = (position[0], position[1] + scale * 52.0)
|
||||
self._name_text.scale = 1.0 * scale * self._name_scale
|
||||
if self._show_lives:
|
||||
self._lives_text.position = (
|
||||
position[0] + scale * 10.0,
|
||||
position[1] - scale * 43.0,
|
||||
)
|
||||
self._lives_text.scale = 1.0 * scale
|
||||
|
||||
def update_for_lives(self) -> None:
|
||||
"""Update for the target player's current lives."""
|
||||
if self._player:
|
||||
lives = self._player.lives
|
||||
else:
|
||||
lives = 0
|
||||
if self._show_lives:
|
||||
if lives > 0:
|
||||
self._lives_text.text = 'x' + str(lives - 1)
|
||||
else:
|
||||
self._lives_text.text = ''
|
||||
if lives == 0:
|
||||
self._name_text.opacity = 0.2
|
||||
assert self.node
|
||||
self.node.color = (0.7, 0.3, 0.3)
|
||||
self.node.opacity = 0.2
|
||||
|
||||
def handle_player_spawned(self) -> None:
|
||||
"""Our player spawned; hooray!"""
|
||||
if not self.node:
|
||||
return
|
||||
self.node.opacity = 1.0
|
||||
self.update_for_lives()
|
||||
|
||||
def handle_player_died(self) -> None:
|
||||
"""Well poo; our player died."""
|
||||
if not self.node:
|
||||
return
|
||||
if self._show_death:
|
||||
bs.animate(
|
||||
self.node,
|
||||
'opacity',
|
||||
{
|
||||
0.00: 1.0,
|
||||
0.05: 0.0,
|
||||
0.10: 1.0,
|
||||
0.15: 0.0,
|
||||
0.20: 1.0,
|
||||
0.25: 0.0,
|
||||
0.30: 1.0,
|
||||
0.35: 0.0,
|
||||
0.40: 1.0,
|
||||
0.45: 0.0,
|
||||
0.50: 1.0,
|
||||
0.55: 0.2,
|
||||
},
|
||||
)
|
||||
lives = self._player.lives
|
||||
if lives == 0:
|
||||
bs.timer(0.6, self.update_for_lives)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.DieMessage):
|
||||
self.node.delete()
|
||||
return None
|
||||
return super().handlemessage(msg)
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.lives = 0
|
||||
self.icons: list[Icon] = []
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.survival_seconds: int | None = None
|
||||
self.spawn_order: list[Player] = []
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class EliminationGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""Game type where last player(s) left alive win."""
|
||||
|
||||
name = 'Elimination'
|
||||
description = 'Last remaining alive wins.'
|
||||
scoreconfig = bs.ScoreConfig(
|
||||
label='Survived', scoretype=bs.ScoreType.SECONDS, none_is_winner=True
|
||||
)
|
||||
# Show messages when players die since it's meaningful here.
|
||||
announce_player_deaths = True
|
||||
|
||||
allow_mid_activity_joins = False
|
||||
|
||||
@classmethod
|
||||
def get_available_settings(
|
||||
cls, sessiontype: type[bs.Session]
|
||||
) -> list[bs.Setting]:
|
||||
settings = [
|
||||
bs.IntSetting(
|
||||
'Lives Per Player',
|
||||
default=1,
|
||||
min_value=1,
|
||||
max_value=10,
|
||||
increment=1,
|
||||
),
|
||||
bs.IntChoiceSetting(
|
||||
'Time Limit',
|
||||
choices=[
|
||||
('None', 0),
|
||||
('1 Minute', 60),
|
||||
('2 Minutes', 120),
|
||||
('5 Minutes', 300),
|
||||
('10 Minutes', 600),
|
||||
('20 Minutes', 1200),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
bs.FloatChoiceSetting(
|
||||
'Respawn Times',
|
||||
choices=[
|
||||
('Shorter', 0.25),
|
||||
('Short', 0.5),
|
||||
('Normal', 1.0),
|
||||
('Long', 2.0),
|
||||
('Longer', 4.0),
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
bs.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
if issubclass(sessiontype, bs.DualTeamSession):
|
||||
settings.append(bs.BoolSetting('Solo Mode', default=False))
|
||||
settings.append(
|
||||
bs.BoolSetting('Balance Total Lives', default=False)
|
||||
)
|
||||
return settings
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
return issubclass(sessiontype, bs.DualTeamSession) or issubclass(
|
||||
sessiontype, bs.FreeForAllSession
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
assert bs.app.classic is not None
|
||||
return bs.app.classic.getmaps('melee')
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
self._scoreboard = Scoreboard()
|
||||
self._start_time: float | None = None
|
||||
self._vs_text: bs.Actor | None = None
|
||||
self._round_end_timer: bs.Timer | None = None
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self._lives_per_player = int(settings['Lives Per Player'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
self._balance_total_lives = bool(
|
||||
settings.get('Balance Total Lives', False)
|
||||
)
|
||||
self._solo_mode = bool(settings.get('Solo Mode', False))
|
||||
|
||||
# Base class overrides:
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL
|
||||
)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
return (
|
||||
'Last team standing wins.'
|
||||
if isinstance(self.session, bs.DualTeamSession)
|
||||
else 'Last one standing wins.'
|
||||
)
|
||||
|
||||
def get_instance_description_short(self) -> str | Sequence:
|
||||
return (
|
||||
'last team standing wins'
|
||||
if isinstance(self.session, bs.DualTeamSession)
|
||||
else 'last one standing wins'
|
||||
)
|
||||
|
||||
def on_player_join(self, player: Player) -> None:
|
||||
player.lives = self._lives_per_player
|
||||
|
||||
if self._solo_mode:
|
||||
player.team.spawn_order.append(player)
|
||||
self._update_solo_mode()
|
||||
else:
|
||||
# Create our icon and spawn.
|
||||
player.icons = [Icon(player, position=(0, 50), scale=0.8)]
|
||||
if player.lives > 0:
|
||||
self.spawn_player(player)
|
||||
|
||||
# Don't waste time doing this until begin.
|
||||
if self.has_begun():
|
||||
self._update_icons()
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
self._start_time = bs.time()
|
||||
self.setup_standard_time_limit(self._time_limit)
|
||||
self.setup_standard_powerup_drops()
|
||||
if self._solo_mode:
|
||||
self._vs_text = bs.NodeActor(
|
||||
bs.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'position': (0, 105),
|
||||
'h_attach': 'center',
|
||||
'h_align': 'center',
|
||||
'maxwidth': 200,
|
||||
'shadow': 0.5,
|
||||
'vr_depth': 390,
|
||||
'scale': 0.6,
|
||||
'v_attach': 'bottom',
|
||||
'color': (0.8, 0.8, 0.3, 1.0),
|
||||
'text': bs.Lstr(resource='vsText'),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# If balance-team-lives is on, add lives to the smaller team until
|
||||
# total lives match.
|
||||
if (
|
||||
isinstance(self.session, bs.DualTeamSession)
|
||||
and self._balance_total_lives
|
||||
and self.teams[0].players
|
||||
and self.teams[1].players
|
||||
):
|
||||
if self._get_total_team_lives(
|
||||
self.teams[0]
|
||||
) < self._get_total_team_lives(self.teams[1]):
|
||||
lesser_team = self.teams[0]
|
||||
greater_team = self.teams[1]
|
||||
else:
|
||||
lesser_team = self.teams[1]
|
||||
greater_team = self.teams[0]
|
||||
add_index = 0
|
||||
while self._get_total_team_lives(
|
||||
lesser_team
|
||||
) < self._get_total_team_lives(greater_team):
|
||||
lesser_team.players[add_index].lives += 1
|
||||
add_index = (add_index + 1) % len(lesser_team.players)
|
||||
|
||||
self._update_icons()
|
||||
|
||||
# We could check game-over conditions at explicit trigger points,
|
||||
# but lets just do the simple thing and poll it.
|
||||
bs.timer(1.0, self._update, repeat=True)
|
||||
|
||||
def _update_solo_mode(self) -> None:
|
||||
# For both teams, find the first player on the spawn order list with
|
||||
# lives remaining and spawn them if they're not alive.
|
||||
for team in self.teams:
|
||||
# Prune dead players from the spawn order.
|
||||
team.spawn_order = [p for p in team.spawn_order if p]
|
||||
for player in team.spawn_order:
|
||||
assert isinstance(player, Player)
|
||||
if player.lives > 0:
|
||||
if not player.is_alive():
|
||||
self.spawn_player(player)
|
||||
break
|
||||
|
||||
def _update_icons(self) -> None:
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
# In free-for-all mode, everyone is just lined up along the bottom.
|
||||
if isinstance(self.session, bs.FreeForAllSession):
|
||||
count = len(self.teams)
|
||||
x_offs = 85
|
||||
xval = x_offs * (count - 1) * -0.5
|
||||
for team in self.teams:
|
||||
if len(team.players) == 1:
|
||||
player = team.players[0]
|
||||
for icon in player.icons:
|
||||
icon.set_position_and_scale((xval, 30), 0.7)
|
||||
icon.update_for_lives()
|
||||
xval += x_offs
|
||||
|
||||
# In teams mode we split up teams.
|
||||
else:
|
||||
if self._solo_mode:
|
||||
# First off, clear out all icons.
|
||||
for player in self.players:
|
||||
player.icons = []
|
||||
|
||||
# Now for each team, cycle through our available players
|
||||
# adding icons.
|
||||
for team in self.teams:
|
||||
if team.id == 0:
|
||||
xval = -60
|
||||
x_offs = -78
|
||||
else:
|
||||
xval = 60
|
||||
x_offs = 78
|
||||
is_first = True
|
||||
test_lives = 1
|
||||
while True:
|
||||
players_with_lives = [
|
||||
p
|
||||
for p in team.spawn_order
|
||||
if p and p.lives >= test_lives
|
||||
]
|
||||
if not players_with_lives:
|
||||
break
|
||||
for player in players_with_lives:
|
||||
player.icons.append(
|
||||
Icon(
|
||||
player,
|
||||
position=(xval, (40 if is_first else 25)),
|
||||
scale=1.0 if is_first else 0.5,
|
||||
name_maxwidth=130 if is_first else 75,
|
||||
name_scale=0.8 if is_first else 1.0,
|
||||
flatness=0.0 if is_first else 1.0,
|
||||
shadow=0.5 if is_first else 1.0,
|
||||
show_death=is_first,
|
||||
show_lives=False,
|
||||
)
|
||||
)
|
||||
xval += x_offs * (0.8 if is_first else 0.56)
|
||||
is_first = False
|
||||
test_lives += 1
|
||||
# Non-solo mode.
|
||||
else:
|
||||
for team in self.teams:
|
||||
if team.id == 0:
|
||||
xval = -50
|
||||
x_offs = -85
|
||||
else:
|
||||
xval = 50
|
||||
x_offs = 85
|
||||
for player in team.players:
|
||||
for icon in player.icons:
|
||||
icon.set_position_and_scale((xval, 30), 0.7)
|
||||
icon.update_for_lives()
|
||||
xval += x_offs
|
||||
|
||||
def _get_spawn_point(self, player: Player) -> bs.Vec3 | None:
|
||||
del player # Unused.
|
||||
|
||||
# In solo-mode, if there's an existing live player on the map, spawn at
|
||||
# whichever spot is farthest from them (keeps the action spread out).
|
||||
if self._solo_mode:
|
||||
living_player = None
|
||||
living_player_pos = None
|
||||
for team in self.teams:
|
||||
for tplayer in team.players:
|
||||
if tplayer.is_alive():
|
||||
assert tplayer.node
|
||||
ppos = tplayer.node.position
|
||||
living_player = tplayer
|
||||
living_player_pos = ppos
|
||||
break
|
||||
if living_player:
|
||||
assert living_player_pos is not None
|
||||
player_pos = bs.Vec3(living_player_pos)
|
||||
points: list[tuple[float, bs.Vec3]] = []
|
||||
for team in self.teams:
|
||||
start_pos = bs.Vec3(self.map.get_start_position(team.id))
|
||||
points.append(
|
||||
((start_pos - player_pos).length(), start_pos)
|
||||
)
|
||||
# Hmm.. we need to sorting vectors too?
|
||||
points.sort(key=lambda x: x[0])
|
||||
return points[-1][1]
|
||||
return None
|
||||
|
||||
def spawn_player(self, player: Player) -> bs.Actor:
|
||||
actor = self.spawn_player_spaz(player, self._get_spawn_point(player))
|
||||
if not self._solo_mode:
|
||||
bs.timer(0.3, bs.Call(self._print_lives, player))
|
||||
|
||||
# If we have any icons, update their state.
|
||||
for icon in player.icons:
|
||||
icon.handle_player_spawned()
|
||||
return actor
|
||||
|
||||
def _print_lives(self, player: Player) -> None:
|
||||
from bascenev1lib.actor import popuptext
|
||||
|
||||
# We get called in a timer so it's possible our player has left/etc.
|
||||
if not player or not player.is_alive() or not player.node:
|
||||
return
|
||||
|
||||
popuptext.PopupText(
|
||||
'x' + str(player.lives - 1),
|
||||
color=(1, 1, 0, 1),
|
||||
offset=(0, -0.8, 0),
|
||||
random_offset=0.0,
|
||||
scale=1.8,
|
||||
position=player.node.position,
|
||||
).autoretain()
|
||||
|
||||
def on_player_leave(self, player: Player) -> None:
|
||||
super().on_player_leave(player)
|
||||
player.icons = []
|
||||
|
||||
# Remove us from spawn-order.
|
||||
if self._solo_mode:
|
||||
if player in player.team.spawn_order:
|
||||
player.team.spawn_order.remove(player)
|
||||
|
||||
# Update icons in a moment since our team will be gone from the
|
||||
# list then.
|
||||
bs.timer(0, self._update_icons)
|
||||
|
||||
# If the player to leave was the last in spawn order and had
|
||||
# their final turn currently in-progress, mark the survival time
|
||||
# for their team.
|
||||
if self._get_total_team_lives(player.team) == 0:
|
||||
assert self._start_time is not None
|
||||
player.team.survival_seconds = int(bs.time() - self._start_time)
|
||||
|
||||
def _get_total_team_lives(self, team: Team) -> int:
|
||||
return sum(player.lives for player in team.players)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
player: Player = msg.getplayer(Player)
|
||||
|
||||
player.lives -= 1
|
||||
if player.lives < 0:
|
||||
logging.exception(
|
||||
"Got lives < 0 in Elim; this shouldn't happen. solo: %s",
|
||||
self._solo_mode,
|
||||
)
|
||||
player.lives = 0
|
||||
|
||||
# If we have any icons, update their state.
|
||||
for icon in player.icons:
|
||||
icon.handle_player_died()
|
||||
|
||||
# Play big death sound on our last death
|
||||
# or for every one in solo mode.
|
||||
if self._solo_mode or player.lives == 0:
|
||||
SpazFactory.get().single_player_death_sound.play()
|
||||
|
||||
# If we hit zero lives, we're dead (and our team might be too).
|
||||
if player.lives == 0:
|
||||
# If the whole team is now dead, mark their survival time.
|
||||
if self._get_total_team_lives(player.team) == 0:
|
||||
assert self._start_time is not None
|
||||
player.team.survival_seconds = int(
|
||||
bs.time() - self._start_time
|
||||
)
|
||||
else:
|
||||
# Otherwise, in regular mode, respawn.
|
||||
if not self._solo_mode:
|
||||
self.respawn_player(player)
|
||||
|
||||
# In solo, put ourself at the back of the spawn order.
|
||||
if self._solo_mode:
|
||||
player.team.spawn_order.remove(player)
|
||||
player.team.spawn_order.append(player)
|
||||
|
||||
def _update(self) -> None:
|
||||
if self._solo_mode:
|
||||
# For both teams, find the first player on the spawn order
|
||||
# list with lives remaining and spawn them if they're not alive.
|
||||
for team in self.teams:
|
||||
# Prune dead players from the spawn order.
|
||||
team.spawn_order = [p for p in team.spawn_order if p]
|
||||
for player in team.spawn_order:
|
||||
assert isinstance(player, Player)
|
||||
if player.lives > 0:
|
||||
if not player.is_alive():
|
||||
self.spawn_player(player)
|
||||
self._update_icons()
|
||||
break
|
||||
|
||||
# If we're down to 1 or fewer living teams, start a timer to end
|
||||
# the game (allows the dust to settle and draws to occur if deaths
|
||||
# are close enough).
|
||||
if len(self._get_living_teams()) < 2:
|
||||
self._round_end_timer = bs.Timer(0.5, self.end_game)
|
||||
|
||||
def _get_living_teams(self) -> list[Team]:
|
||||
return [
|
||||
team
|
||||
for team in self.teams
|
||||
if len(team.players) > 0
|
||||
and any(player.lives > 0 for player in team.players)
|
||||
]
|
||||
|
||||
def end_game(self) -> None:
|
||||
if self.has_ended():
|
||||
return
|
||||
results = bs.GameResults()
|
||||
self._vs_text = None # Kill our 'vs' if its there.
|
||||
for team in self.teams:
|
||||
results.set_team_score(team, team.survival_seconds)
|
||||
self.end(results=results)
|
||||
989
dist/ba_data/python/bascenev1lib/game/football.py
vendored
Normal file
989
dist/ba_data/python/bascenev1lib/game/football.py
vendored
Normal file
|
|
@ -0,0 +1,989 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Implements football games (both co-op and teams varieties)."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.bomb import TNTSpawner
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.actor.respawnicon import RespawnIcon
|
||||
from bascenev1lib.actor.powerupbox import PowerupBoxFactory, PowerupBox
|
||||
from bascenev1lib.actor.flag import (
|
||||
FlagFactory,
|
||||
Flag,
|
||||
FlagPickedUpMessage,
|
||||
FlagDroppedMessage,
|
||||
FlagDiedMessage,
|
||||
)
|
||||
from bascenev1lib.actor.spazbot import (
|
||||
SpazBotDiedMessage,
|
||||
SpazBotPunchedMessage,
|
||||
SpazBotSet,
|
||||
BrawlerBotLite,
|
||||
BrawlerBot,
|
||||
BomberBotLite,
|
||||
BomberBot,
|
||||
TriggerBot,
|
||||
ChargerBot,
|
||||
TriggerBotPro,
|
||||
BrawlerBotPro,
|
||||
StickyBot,
|
||||
ExplodeyBot,
|
||||
)
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
from bascenev1lib.actor.spaz import Spaz
|
||||
from bascenev1lib.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: bs.Player | None = None
|
||||
self.node.is_area_of_interest = True
|
||||
self.respawn_timer: bs.Timer | None = None
|
||||
self.scored = False
|
||||
self.held_count = 0
|
||||
self.light = bs.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(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.respawn_timer: bs.Timer | None = None
|
||||
self.respawn_icon: RespawnIcon | None = None
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.score = 0
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class FootballTeamGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""Football game for teams mode."""
|
||||
|
||||
name = 'Football'
|
||||
description = 'Get the flag to the enemy end zone.'
|
||||
available_settings = [
|
||||
bs.IntSetting(
|
||||
'Score to Win',
|
||||
min_value=7,
|
||||
default=21,
|
||||
increment=7,
|
||||
),
|
||||
bs.IntChoiceSetting(
|
||||
'Time Limit',
|
||||
choices=[
|
||||
('None', 0),
|
||||
('1 Minute', 60),
|
||||
('2 Minutes', 120),
|
||||
('5 Minutes', 300),
|
||||
('10 Minutes', 600),
|
||||
('20 Minutes', 1200),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
bs.FloatChoiceSetting(
|
||||
'Respawn Times',
|
||||
choices=[
|
||||
('Shorter', 0.25),
|
||||
('Short', 0.5),
|
||||
('Normal', 1.0),
|
||||
('Long', 2.0),
|
||||
('Longer', 4.0),
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
bs.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
# We only support two-team play.
|
||||
return issubclass(sessiontype, bs.DualTeamSession)
|
||||
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
assert bs.app.classic is not None
|
||||
return bs.app.classic.getmaps('football')
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
self._scoreboard: Scoreboard | None = Scoreboard()
|
||||
|
||||
# Load some media we need.
|
||||
self._cheer_sound = bs.getsound('cheer')
|
||||
self._chant_sound = bs.getsound('crowdChant')
|
||||
self._score_sound = bs.getsound('score')
|
||||
self._swipsound = bs.getsound('swip')
|
||||
self._whistle_sound = bs.getsound('refWhistle')
|
||||
self._score_region_material = bs.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[bs.NodeActor] = []
|
||||
self._flag: FootballFlag | None = None
|
||||
self._flag_respawn_timer: bs.Timer | None = None
|
||||
self._flag_respawn_light: bs.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 = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.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(
|
||||
bs.NodeActor(
|
||||
bs.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(
|
||||
bs.NodeActor(
|
||||
bs.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()
|
||||
self._chant_sound.play()
|
||||
|
||||
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 = bs.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(bs.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()
|
||||
self._score_sound.play()
|
||||
self._cheer_sound.play()
|
||||
assert self._flag
|
||||
self._flag.scored = True
|
||||
|
||||
# Kill the flag (it'll respawn shortly).
|
||||
bs.timer(1.0, self._kill_flag)
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': bs.getcollision().position,
|
||||
'height_attenuated': False,
|
||||
'color': (1, 0, 0),
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0.0: 0, 0.5: 1, 1.0: 0}, loop=True)
|
||||
bs.timer(1.0, light.delete)
|
||||
bs.cameraflash(duration=10.0)
|
||||
self._update_scoreboard()
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = bs.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 bs.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, bs.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 = bs.Timer(3.0, self._spawn_flag)
|
||||
self._flag_respawn_light = bs.NodeActor(
|
||||
bs.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
|
||||
bs.animate(
|
||||
self._flag_respawn_light.node,
|
||||
'intensity',
|
||||
{0.0: 0, 0.25: 0.15, 0.5: 0},
|
||||
loop=True,
|
||||
)
|
||||
bs.timer(3.0, self._flag_respawn_light.node.delete)
|
||||
|
||||
else:
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
|
||||
def _flash_flag_spawn(self) -> None:
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': self._flag_spawn_pos,
|
||||
'height_attenuated': False,
|
||||
'color': (1, 1, 0),
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True)
|
||||
bs.timer(1.0, light.delete)
|
||||
|
||||
def _spawn_flag(self) -> None:
|
||||
self._swipsound.play()
|
||||
self._whistle_sound.play()
|
||||
self._flash_flag_spawn()
|
||||
assert self._flag_spawn_pos is not None
|
||||
self._flag = FootballFlag(position=self._flag_spawn_pos)
|
||||
|
||||
|
||||
class FootballCoopGame(bs.CoopGameActivity[Player, Team]):
|
||||
"""Co-op variant of football."""
|
||||
|
||||
name = 'Football'
|
||||
tips = ['Use the pick-up button to grab the flag < ${PICKUP} >']
|
||||
scoreconfig = bs.ScoreConfig(
|
||||
scoretype=bs.ScoreType.MILLISECONDS, version='B'
|
||||
)
|
||||
|
||||
default_music = bs.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 = bs.getsound('cheer')
|
||||
self._boo_sound = bs.getsound('boo')
|
||||
self._chant_sound = bs.getsound('crowdChant')
|
||||
self._score_sound = bs.getsound('score')
|
||||
self._swipsound = bs.getsound('swip')
|
||||
self._whistle_sound = bs.getsound('refWhistle')
|
||||
self._score_to_win = 21
|
||||
self._score_region_material = bs.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[bs.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: bs.NodeActor | None = None
|
||||
self._time_text_input: bs.NodeActor | None = None
|
||||
self._tntspawner: TNTSpawner | None = None
|
||||
self._bots = SpazBotSet()
|
||||
self._bot_spawn_timer: bs.Timer | None = None
|
||||
self._powerup_drop_timer: bs.Timer | None = None
|
||||
self._scoring_team: Team | None = None
|
||||
self._final_time_ms: int | None = None
|
||||
self._time_text_timer: bs.Timer | None = None
|
||||
self._flag_respawn_light: bs.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(
|
||||
bs.NodeActor(
|
||||
bs.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(
|
||||
bs.NodeActor(
|
||||
bs.newnode(
|
||||
'region',
|
||||
attrs={
|
||||
'position': defs.boxes['goal2'][0:3],
|
||||
'scale': defs.boxes['goal2'][6:9],
|
||||
'type': 'box',
|
||||
'materials': [self._score_region_material],
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
self._chant_sound.play()
|
||||
|
||||
def on_begin(self) -> None:
|
||||
# FIXME: Split this up a bit.
|
||||
# pylint: disable=too-many-statements
|
||||
from bascenev1lib.actor import controlsguide
|
||||
|
||||
super().on_begin()
|
||||
|
||||
# Show controls help in kiosk mode.
|
||||
if bs.app.demo_mode or bs.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 RuntimeError()
|
||||
|
||||
self.setup_low_life_warning_sound()
|
||||
|
||||
self._drop_powerups(standard_points=True)
|
||||
bs.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 = int(bs.time() * 1000.0)
|
||||
assert isinstance(starttime_ms, int)
|
||||
self._starttime_ms = starttime_ms
|
||||
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.3,
|
||||
'text': '',
|
||||
},
|
||||
)
|
||||
)
|
||||
self._time_text_input = bs.NodeActor(
|
||||
bs.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 = bs.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 = bs.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 = bs.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 = bs.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 = bs.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):
|
||||
bs.timer(
|
||||
1.0 + i * 0.5, bs.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(bs.DieMessage())
|
||||
except Exception:
|
||||
logging.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 = bs.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(bs.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)
|
||||
|
||||
self._score_sound.play()
|
||||
if i == 0:
|
||||
self._cheer_sound.play()
|
||||
else:
|
||||
self._boo_sound.play()
|
||||
|
||||
# Kill the flag (it'll respawn shortly).
|
||||
self._flag.scored = True
|
||||
|
||||
bs.timer(0.2, self._kill_flag)
|
||||
|
||||
self.update_scores()
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': bs.getcollision().position,
|
||||
'height_attenuated': False,
|
||||
'color': (1, 0, 0),
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True)
|
||||
bs.timer(1.0, light.delete)
|
||||
if i == 0:
|
||||
bs.cameraflash(duration=10.0)
|
||||
|
||||
def end_game(self) -> None:
|
||||
bs.setmusic(None)
|
||||
self._bots.final_celebrate()
|
||||
bs.timer(0.001, bs.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:
|
||||
bs.setmusic(bs.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(
|
||||
bs.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(
|
||||
int(bs.time() * 1000.0) - 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?
|
||||
bs.pushcall(bs.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, bs.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 = bs.Timer(
|
||||
respawn_time, bs.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.
|
||||
bs.timer(3.0, bs.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 = bs.Timer(3.0, self._spawn_flag)
|
||||
self._flag_respawn_light = bs.NodeActor(
|
||||
bs.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
|
||||
bs.animate(
|
||||
self._flag_respawn_light.node,
|
||||
'intensity',
|
||||
{0: 0, 0.25: 0.15, 0.5: 0},
|
||||
loop=True,
|
||||
)
|
||||
bs.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: bs.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) -> bs.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 = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': self._flag_spawn_pos,
|
||||
'height_attenuated': False,
|
||||
'color': (1, 1, 0),
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True)
|
||||
bs.timer(1.0, light.delete)
|
||||
|
||||
def _spawn_flag(self) -> None:
|
||||
self._swipsound.play()
|
||||
self._whistle_sound.play()
|
||||
self._flash_flag_spawn()
|
||||
assert self._flag_spawn_pos is not None
|
||||
self._flag = FootballFlag(position=self._flag_spawn_pos)
|
||||
409
dist/ba_data/python/bascenev1lib/game/hockey.py
vendored
Normal file
409
dist/ba_data/python/bascenev1lib/game/hockey.py
vendored
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Hockey game and support classes."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.actor.powerupbox import PowerupBoxFactory
|
||||
from bascenev1lib.gameutils import SharedObjects
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class PuckDiedMessage:
|
||||
"""Inform something that a puck has died."""
|
||||
|
||||
def __init__(self, puck: Puck):
|
||||
self.puck = puck
|
||||
|
||||
|
||||
class Puck(bs.Actor):
|
||||
"""A lovely giant hockey puck."""
|
||||
|
||||
def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)):
|
||||
super().__init__()
|
||||
shared = SharedObjects.get()
|
||||
activity = self.getactivity()
|
||||
|
||||
# Spawn just above the provided point.
|
||||
self._spawn_pos = (position[0], position[1] + 1.0, position[2])
|
||||
self.last_players_to_touch: dict[int, Player] = {}
|
||||
self.scored = False
|
||||
assert activity is not None
|
||||
assert isinstance(activity, HockeyGame)
|
||||
pmats = [shared.object_material, activity.puck_material]
|
||||
self.node = bs.newnode(
|
||||
'prop',
|
||||
delegate=self,
|
||||
attrs={
|
||||
'mesh': activity.puck_mesh,
|
||||
'color_texture': activity.puck_tex,
|
||||
'body': 'puck',
|
||||
'reflection': 'soft',
|
||||
'reflection_scale': [0.2],
|
||||
'shadow_size': 1.0,
|
||||
'is_area_of_interest': True,
|
||||
'position': self._spawn_pos,
|
||||
'materials': pmats,
|
||||
},
|
||||
)
|
||||
bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1})
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.DieMessage):
|
||||
assert self.node
|
||||
self.node.delete()
|
||||
activity = self._activity()
|
||||
if activity and not msg.immediate:
|
||||
activity.handlemessage(PuckDiedMessage(self))
|
||||
|
||||
# If we go out of bounds, move back to where we started.
|
||||
elif isinstance(msg, bs.OutOfBoundsMessage):
|
||||
assert self.node
|
||||
self.node.position = self._spawn_pos
|
||||
|
||||
elif isinstance(msg, bs.HitMessage):
|
||||
assert self.node
|
||||
assert msg.force_direction is not None
|
||||
self.node.handlemessage(
|
||||
'impulse',
|
||||
msg.pos[0],
|
||||
msg.pos[1],
|
||||
msg.pos[2],
|
||||
msg.velocity[0],
|
||||
msg.velocity[1],
|
||||
msg.velocity[2],
|
||||
1.0 * msg.magnitude,
|
||||
1.0 * msg.velocity_magnitude,
|
||||
msg.radius,
|
||||
0,
|
||||
msg.force_direction[0],
|
||||
msg.force_direction[1],
|
||||
msg.force_direction[2],
|
||||
)
|
||||
|
||||
# If this hit came from a player, log them as the last to touch us.
|
||||
s_player = msg.get_source_player(Player)
|
||||
if s_player is not None:
|
||||
activity = self._activity()
|
||||
if activity:
|
||||
if s_player in activity.players:
|
||||
self.last_players_to_touch[s_player.team.id] = s_player
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.score = 0
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class HockeyGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""Ice hockey game."""
|
||||
|
||||
name = 'Hockey'
|
||||
description = 'Score some goals.'
|
||||
available_settings = [
|
||||
bs.IntSetting(
|
||||
'Score to Win',
|
||||
min_value=1,
|
||||
default=1,
|
||||
increment=1,
|
||||
),
|
||||
bs.IntChoiceSetting(
|
||||
'Time Limit',
|
||||
choices=[
|
||||
('None', 0),
|
||||
('1 Minute', 60),
|
||||
('2 Minutes', 120),
|
||||
('5 Minutes', 300),
|
||||
('10 Minutes', 600),
|
||||
('20 Minutes', 1200),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
bs.FloatChoiceSetting(
|
||||
'Respawn Times',
|
||||
choices=[
|
||||
('Shorter', 0.25),
|
||||
('Short', 0.5),
|
||||
('Normal', 1.0),
|
||||
('Long', 2.0),
|
||||
('Longer', 4.0),
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
bs.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
return issubclass(sessiontype, bs.DualTeamSession)
|
||||
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
assert bs.app.classic is not None
|
||||
return bs.app.classic.getmaps('hockey')
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
shared = SharedObjects.get()
|
||||
self._scoreboard = Scoreboard()
|
||||
self._cheer_sound = bs.getsound('cheer')
|
||||
self._chant_sound = bs.getsound('crowdChant')
|
||||
self._foghorn_sound = bs.getsound('foghorn')
|
||||
self._swipsound = bs.getsound('swip')
|
||||
self._whistle_sound = bs.getsound('refWhistle')
|
||||
self.puck_mesh = bs.getmesh('puck')
|
||||
self.puck_tex = bs.gettexture('puckColor')
|
||||
self._puck_sound = bs.getsound('metalHit')
|
||||
self.puck_material = bs.Material()
|
||||
self.puck_material.add_actions(
|
||||
actions=('modify_part_collision', 'friction', 0.5)
|
||||
)
|
||||
self.puck_material.add_actions(
|
||||
conditions=('they_have_material', shared.pickup_material),
|
||||
actions=('modify_part_collision', 'collide', False),
|
||||
)
|
||||
self.puck_material.add_actions(
|
||||
conditions=(
|
||||
('we_are_younger_than', 100),
|
||||
'and',
|
||||
('they_have_material', shared.object_material),
|
||||
),
|
||||
actions=('modify_node_collision', 'collide', False),
|
||||
)
|
||||
self.puck_material.add_actions(
|
||||
conditions=('they_have_material', shared.footing_material),
|
||||
actions=('impact_sound', self._puck_sound, 0.2, 5),
|
||||
)
|
||||
|
||||
# Keep track of which player last touched the puck
|
||||
self.puck_material.add_actions(
|
||||
conditions=('they_have_material', shared.player_material),
|
||||
actions=(('call', 'at_connect', self._handle_puck_player_collide),),
|
||||
)
|
||||
|
||||
# We want the puck to kill powerups; not get stopped by them
|
||||
self.puck_material.add_actions(
|
||||
conditions=(
|
||||
'they_have_material',
|
||||
PowerupBoxFactory.get().powerup_material,
|
||||
),
|
||||
actions=(
|
||||
('modify_part_collision', 'physical', False),
|
||||
('message', 'their_node', 'at_connect', bs.DieMessage()),
|
||||
),
|
||||
)
|
||||
self._score_region_material = bs.Material()
|
||||
self._score_region_material.add_actions(
|
||||
conditions=('they_have_material', self.puck_material),
|
||||
actions=(
|
||||
('modify_part_collision', 'collide', True),
|
||||
('modify_part_collision', 'physical', False),
|
||||
('call', 'at_connect', self._handle_score),
|
||||
),
|
||||
)
|
||||
self._puck_spawn_pos: Sequence[float] | None = None
|
||||
self._score_regions: list[bs.NodeActor] | None = None
|
||||
self._puck: Puck | 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 = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.HOCKEY
|
||||
)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
if self._score_to_win == 1:
|
||||
return 'Score a goal.'
|
||||
return 'Score ${ARG1} goals.', self._score_to_win
|
||||
|
||||
def get_instance_description_short(self) -> str | Sequence:
|
||||
if self._score_to_win == 1:
|
||||
return 'score a goal'
|
||||
return 'score ${ARG1} goals', self._score_to_win
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
|
||||
self.setup_standard_time_limit(self._time_limit)
|
||||
self.setup_standard_powerup_drops()
|
||||
self._puck_spawn_pos = self.map.get_flag_position(None)
|
||||
self._spawn_puck()
|
||||
|
||||
# Set up the two score regions.
|
||||
defs = self.map.defs
|
||||
self._score_regions = []
|
||||
self._score_regions.append(
|
||||
bs.NodeActor(
|
||||
bs.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(
|
||||
bs.NodeActor(
|
||||
bs.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()
|
||||
self._chant_sound.play()
|
||||
|
||||
def on_team_join(self, team: Team) -> None:
|
||||
self._update_scoreboard()
|
||||
|
||||
def _handle_puck_player_collide(self) -> None:
|
||||
collision = bs.getcollision()
|
||||
try:
|
||||
puck = collision.sourcenode.getdelegate(Puck, True)
|
||||
player = collision.opposingnode.getdelegate(
|
||||
PlayerSpaz, True
|
||||
).getplayer(Player, True)
|
||||
except bs.NotFoundError:
|
||||
return
|
||||
|
||||
puck.last_players_to_touch[player.team.id] = player
|
||||
|
||||
def _kill_puck(self) -> None:
|
||||
self._puck = None
|
||||
|
||||
def _handle_score(self) -> None:
|
||||
"""A point has been scored."""
|
||||
|
||||
assert self._puck is not None
|
||||
assert self._score_regions is not None
|
||||
|
||||
# Our puck might stick around for a second or two
|
||||
# we don't want it to be able to score again.
|
||||
if self._puck.scored:
|
||||
return
|
||||
|
||||
region = bs.getcollision().sourcenode
|
||||
index = 0
|
||||
for index, score_region in enumerate(self._score_regions):
|
||||
if region == score_region.node:
|
||||
break
|
||||
|
||||
for team in self.teams:
|
||||
if team.id == index:
|
||||
scoring_team = team
|
||||
team.score += 1
|
||||
|
||||
# Tell all players to celebrate.
|
||||
for player in team.players:
|
||||
if player.actor:
|
||||
player.actor.handlemessage(bs.CelebrateMessage(2.0))
|
||||
|
||||
# If we've got the player from the scoring team that last
|
||||
# touched us, give them points.
|
||||
if (
|
||||
scoring_team.id in self._puck.last_players_to_touch
|
||||
and self._puck.last_players_to_touch[scoring_team.id]
|
||||
):
|
||||
self.stats.player_scored(
|
||||
self._puck.last_players_to_touch[scoring_team.id],
|
||||
100,
|
||||
big_message=True,
|
||||
)
|
||||
|
||||
# End game if we won.
|
||||
if team.score >= self._score_to_win:
|
||||
self.end_game()
|
||||
|
||||
self._foghorn_sound.play()
|
||||
self._cheer_sound.play()
|
||||
|
||||
self._puck.scored = True
|
||||
|
||||
# Kill the puck (it'll respawn itself shortly).
|
||||
bs.timer(1.0, self._kill_puck)
|
||||
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': bs.getcollision().position,
|
||||
'height_attenuated': False,
|
||||
'color': (1, 0, 0),
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True)
|
||||
bs.timer(1.0, light.delete)
|
||||
|
||||
bs.cameraflash(duration=10.0)
|
||||
self._update_scoreboard()
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = bs.GameResults()
|
||||
for team in self.teams:
|
||||
results.set_team_score(team, team.score)
|
||||
self.end(results=results)
|
||||
|
||||
def _update_scoreboard(self) -> None:
|
||||
winscore = self._score_to_win
|
||||
for team in self.teams:
|
||||
self._scoreboard.set_team_value(team, team.score, winscore)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
# Respawn dead players if they're still in the game.
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
# Augment standard behavior...
|
||||
super().handlemessage(msg)
|
||||
self.respawn_player(msg.getplayer(Player))
|
||||
|
||||
# Respawn dead pucks.
|
||||
elif isinstance(msg, PuckDiedMessage):
|
||||
if not self.has_ended():
|
||||
bs.timer(3.0, self._spawn_puck)
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
|
||||
def _flash_puck_spawn(self) -> None:
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': self._puck_spawn_pos,
|
||||
'height_attenuated': False,
|
||||
'color': (1, 0, 0),
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True)
|
||||
bs.timer(1.0, light.delete)
|
||||
|
||||
def _spawn_puck(self) -> None:
|
||||
self._swipsound.play()
|
||||
self._whistle_sound.play()
|
||||
self._flash_puck_spawn()
|
||||
assert self._puck_spawn_pos is not None
|
||||
self._puck = Puck(position=self._puck_spawn_pos)
|
||||
281
dist/ba_data/python/bascenev1lib/game/keepaway.py
vendored
Normal file
281
dist/ba_data/python/bascenev1lib/game/keepaway.py
vendored
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Defines a keep-away game type."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.actor.flag import (
|
||||
Flag,
|
||||
FlagDroppedMessage,
|
||||
FlagDiedMessage,
|
||||
FlagPickedUpMessage,
|
||||
)
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class FlagState(Enum):
|
||||
"""States our single flag can be in."""
|
||||
|
||||
NEW = 0
|
||||
UNCONTESTED = 1
|
||||
CONTESTED = 2
|
||||
HELD = 3
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self, timeremaining: int) -> None:
|
||||
self.timeremaining = timeremaining
|
||||
self.holdingflag = False
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class KeepAwayGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""Game where you try to keep the flag away from your enemies."""
|
||||
|
||||
name = 'Keep Away'
|
||||
description = 'Carry the flag for a set length of time.'
|
||||
available_settings = [
|
||||
bs.IntSetting(
|
||||
'Hold Time',
|
||||
min_value=10,
|
||||
default=30,
|
||||
increment=10,
|
||||
),
|
||||
bs.IntChoiceSetting(
|
||||
'Time Limit',
|
||||
choices=[
|
||||
('None', 0),
|
||||
('1 Minute', 60),
|
||||
('2 Minutes', 120),
|
||||
('5 Minutes', 300),
|
||||
('10 Minutes', 600),
|
||||
('20 Minutes', 1200),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
bs.FloatChoiceSetting(
|
||||
'Respawn Times',
|
||||
choices=[
|
||||
('Shorter', 0.25),
|
||||
('Short', 0.5),
|
||||
('Normal', 1.0),
|
||||
('Long', 2.0),
|
||||
('Longer', 4.0),
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
bs.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
scoreconfig = bs.ScoreConfig(label='Time Held')
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
return issubclass(sessiontype, bs.DualTeamSession) or issubclass(
|
||||
sessiontype, bs.FreeForAllSession
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
assert bs.app.classic is not None
|
||||
return bs.app.classic.getmaps('keep_away')
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
self._scoreboard = Scoreboard()
|
||||
self._swipsound = bs.getsound('swip')
|
||||
self._tick_sound = bs.getsound('tick')
|
||||
self._countdownsounds = {
|
||||
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'),
|
||||
}
|
||||
self._flag_spawn_pos: Sequence[float] | None = None
|
||||
self._update_timer: bs.Timer | None = None
|
||||
self._holding_players: list[Player] = []
|
||||
self._flag_state: FlagState | None = None
|
||||
self._flag_light: bs.Node | None = None
|
||||
self._scoring_team: Team | None = None
|
||||
self._flag: Flag | None = None
|
||||
self._hold_time = int(settings['Hold Time'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.KEEP_AWAY
|
||||
)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
return 'Carry the flag for ${ARG1} seconds.', self._hold_time
|
||||
|
||||
def get_instance_description_short(self) -> str | Sequence:
|
||||
return 'carry the flag for ${ARG1} seconds', self._hold_time
|
||||
|
||||
def create_team(self, sessionteam: bs.SessionTeam) -> Team:
|
||||
return Team(timeremaining=self._hold_time)
|
||||
|
||||
def on_team_join(self, team: Team) -> None:
|
||||
self._update_scoreboard()
|
||||
|
||||
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()
|
||||
self._update_timer = bs.Timer(1.0, call=self._tick, repeat=True)
|
||||
self._update_flag_state()
|
||||
Flag.project_stand(self._flag_spawn_pos)
|
||||
|
||||
def _tick(self) -> None:
|
||||
self._update_flag_state()
|
||||
|
||||
# Award points to all living players holding the flag.
|
||||
for player in self._holding_players:
|
||||
if player:
|
||||
self.stats.player_scored(
|
||||
player, 3, screenmessage=False, display=False
|
||||
)
|
||||
|
||||
scoreteam = self._scoring_team
|
||||
|
||||
if scoreteam is not None:
|
||||
if scoreteam.timeremaining > 0:
|
||||
self._tick_sound.play()
|
||||
|
||||
scoreteam.timeremaining = max(0, scoreteam.timeremaining - 1)
|
||||
self._update_scoreboard()
|
||||
if scoreteam.timeremaining > 0:
|
||||
assert self._flag is not None
|
||||
self._flag.set_score_text(str(scoreteam.timeremaining))
|
||||
|
||||
# Announce numbers we have sounds for.
|
||||
if scoreteam.timeremaining in self._countdownsounds:
|
||||
self._countdownsounds[scoreteam.timeremaining].play()
|
||||
|
||||
# Winner.
|
||||
if scoreteam.timeremaining <= 0:
|
||||
self.end_game()
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = bs.GameResults()
|
||||
for team in self.teams:
|
||||
results.set_team_score(team, self._hold_time - team.timeremaining)
|
||||
self.end(results=results, announce_delay=0)
|
||||
|
||||
def _update_flag_state(self) -> None:
|
||||
for team in self.teams:
|
||||
team.holdingflag = False
|
||||
self._holding_players = []
|
||||
for player in self.players:
|
||||
holdingflag = False
|
||||
try:
|
||||
assert isinstance(player.actor, (PlayerSpaz, type(None)))
|
||||
if (
|
||||
player.actor
|
||||
and player.actor.node
|
||||
and player.actor.node.hold_node
|
||||
):
|
||||
holdingflag = (
|
||||
player.actor.node.hold_node.getnodetype() == 'flag'
|
||||
)
|
||||
except Exception:
|
||||
logging.exception('Error checking hold flag.')
|
||||
if holdingflag:
|
||||
self._holding_players.append(player)
|
||||
player.team.holdingflag = True
|
||||
|
||||
holdingteams = set(t for t in self.teams if t.holdingflag)
|
||||
prevstate = self._flag_state
|
||||
assert self._flag is not None
|
||||
assert self._flag_light
|
||||
assert self._flag.node
|
||||
if len(holdingteams) > 1:
|
||||
self._flag_state = FlagState.CONTESTED
|
||||
self._scoring_team = None
|
||||
self._flag_light.color = (0.6, 0.6, 0.1)
|
||||
self._flag.node.color = (1.0, 1.0, 0.4)
|
||||
elif len(holdingteams) == 1:
|
||||
holdingteam = list(holdingteams)[0]
|
||||
self._flag_state = FlagState.HELD
|
||||
self._scoring_team = holdingteam
|
||||
self._flag_light.color = bs.normalized_color(holdingteam.color)
|
||||
self._flag.node.color = holdingteam.color
|
||||
else:
|
||||
self._flag_state = FlagState.UNCONTESTED
|
||||
self._scoring_team = None
|
||||
self._flag_light.color = (0.2, 0.2, 0.2)
|
||||
self._flag.node.color = (1, 1, 1)
|
||||
|
||||
if self._flag_state != prevstate:
|
||||
self._swipsound.play()
|
||||
|
||||
def _spawn_flag(self) -> None:
|
||||
self._swipsound.play()
|
||||
self._flash_flag_spawn()
|
||||
assert self._flag_spawn_pos is not None
|
||||
self._flag = Flag(dropped_timeout=20, position=self._flag_spawn_pos)
|
||||
self._flag_state = FlagState.NEW
|
||||
self._flag_light = bs.newnode(
|
||||
'light',
|
||||
owner=self._flag.node,
|
||||
attrs={'intensity': 0.2, 'radius': 0.3, 'color': (0.2, 0.2, 0.2)},
|
||||
)
|
||||
assert self._flag.node
|
||||
self._flag.node.connectattr('position', self._flag_light, 'position')
|
||||
self._update_flag_state()
|
||||
|
||||
def _flash_flag_spawn(self) -> None:
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': self._flag_spawn_pos,
|
||||
'color': (1, 1, 1),
|
||||
'radius': 0.3,
|
||||
'height_attenuated': False,
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0.0: 0, 0.25: 0.5, 0.5: 0}, loop=True)
|
||||
bs.timer(1.0, light.delete)
|
||||
|
||||
def _update_scoreboard(self) -> None:
|
||||
for team in self.teams:
|
||||
self._scoreboard.set_team_value(
|
||||
team, team.timeremaining, self._hold_time, countdown=True
|
||||
)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
self.respawn_player(msg.getplayer(Player))
|
||||
elif isinstance(msg, FlagDiedMessage):
|
||||
self._spawn_flag()
|
||||
elif isinstance(msg, (FlagDroppedMessage, FlagPickedUpMessage)):
|
||||
self._update_flag_state()
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
293
dist/ba_data/python/bascenev1lib/game/kingofthehill.py
vendored
Normal file
293
dist/ba_data/python/bascenev1lib/game/kingofthehill.py
vendored
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Defines the King of the Hill game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import weakref
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.flag import Flag
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.gameutils import SharedObjects
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class FlagState(Enum):
|
||||
"""States our single flag can be in."""
|
||||
|
||||
NEW = 0
|
||||
UNCONTESTED = 1
|
||||
CONTESTED = 2
|
||||
HELD = 3
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.time_at_flag = 0
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self, time_remaining: int) -> None:
|
||||
self.time_remaining = time_remaining
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""Game where a team wins by holding a 'hill' for a set amount of time."""
|
||||
|
||||
name = 'King of the Hill'
|
||||
description = 'Secure the flag for a set length of time.'
|
||||
available_settings = [
|
||||
bs.IntSetting(
|
||||
'Hold Time',
|
||||
min_value=10,
|
||||
default=30,
|
||||
increment=10,
|
||||
),
|
||||
bs.IntChoiceSetting(
|
||||
'Time Limit',
|
||||
choices=[
|
||||
('None', 0),
|
||||
('1 Minute', 60),
|
||||
('2 Minutes', 120),
|
||||
('5 Minutes', 300),
|
||||
('10 Minutes', 600),
|
||||
('20 Minutes', 1200),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
bs.FloatChoiceSetting(
|
||||
'Respawn Times',
|
||||
choices=[
|
||||
('Shorter', 0.25),
|
||||
('Short', 0.5),
|
||||
('Normal', 1.0),
|
||||
('Long', 2.0),
|
||||
('Longer', 4.0),
|
||||
],
|
||||
default=1.0,
|
||||
),
|
||||
bs.BoolSetting('Epic Mode', default=False),
|
||||
]
|
||||
scoreconfig = bs.ScoreConfig(label='Time Held')
|
||||
|
||||
@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]:
|
||||
assert bs.app.classic is not None
|
||||
return bs.app.classic.getmaps('king_of_the_hill')
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
shared = SharedObjects.get()
|
||||
self._scoreboard = Scoreboard()
|
||||
self._swipsound = bs.getsound('swip')
|
||||
self._tick_sound = bs.getsound('tick')
|
||||
self._countdownsounds = {
|
||||
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'),
|
||||
}
|
||||
self._flag_pos: Sequence[float] | None = None
|
||||
self._flag_state: FlagState | None = None
|
||||
self._flag: Flag | None = None
|
||||
self._flag_light: bs.Node | None = None
|
||||
self._scoring_team: weakref.ref[Team] | None = None
|
||||
self._hold_time = int(settings['Hold Time'])
|
||||
self._time_limit = float(settings['Time Limit'])
|
||||
self._epic_mode = bool(settings['Epic Mode'])
|
||||
self._flag_region_material = bs.Material()
|
||||
self._flag_region_material.add_actions(
|
||||
conditions=('they_have_material', shared.player_material),
|
||||
actions=(
|
||||
('modify_part_collision', 'collide', True),
|
||||
('modify_part_collision', 'physical', False),
|
||||
(
|
||||
'call',
|
||||
'at_connect',
|
||||
bs.Call(self._handle_player_flag_region_collide, True),
|
||||
),
|
||||
(
|
||||
'call',
|
||||
'at_disconnect',
|
||||
bs.Call(self._handle_player_flag_region_collide, False),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# Base class overrides.
|
||||
self.slow_motion = self._epic_mode
|
||||
self.default_music = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SCARY
|
||||
)
|
||||
|
||||
def get_instance_description(self) -> str | Sequence:
|
||||
return 'Secure the flag for ${ARG1} seconds.', self._hold_time
|
||||
|
||||
def get_instance_description_short(self) -> str | Sequence:
|
||||
return 'secure the flag for ${ARG1} seconds', self._hold_time
|
||||
|
||||
def create_team(self, sessionteam: bs.SessionTeam) -> Team:
|
||||
return Team(time_remaining=self._hold_time)
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
shared = SharedObjects.get()
|
||||
self.setup_standard_time_limit(self._time_limit)
|
||||
self.setup_standard_powerup_drops()
|
||||
self._flag_pos = self.map.get_flag_position(None)
|
||||
bs.timer(1.0, self._tick, repeat=True)
|
||||
self._flag_state = FlagState.NEW
|
||||
Flag.project_stand(self._flag_pos)
|
||||
self._flag = Flag(
|
||||
position=self._flag_pos, touchable=False, color=(1, 1, 1)
|
||||
)
|
||||
self._flag_light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': self._flag_pos,
|
||||
'intensity': 0.2,
|
||||
'height_attenuated': False,
|
||||
'radius': 0.4,
|
||||
'color': (0.2, 0.2, 0.2),
|
||||
},
|
||||
)
|
||||
# Flag region.
|
||||
flagmats = [self._flag_region_material, shared.region_material]
|
||||
bs.newnode(
|
||||
'region',
|
||||
attrs={
|
||||
'position': self._flag_pos,
|
||||
'scale': (1.8, 1.8, 1.8),
|
||||
'type': 'sphere',
|
||||
'materials': flagmats,
|
||||
},
|
||||
)
|
||||
self._update_flag_state()
|
||||
|
||||
def _tick(self) -> None:
|
||||
self._update_flag_state()
|
||||
|
||||
# Give holding players points.
|
||||
for player in self.players:
|
||||
if player.time_at_flag > 0:
|
||||
self.stats.player_scored(
|
||||
player, 3, screenmessage=False, display=False
|
||||
)
|
||||
if self._scoring_team is None:
|
||||
scoring_team = None
|
||||
else:
|
||||
scoring_team = self._scoring_team()
|
||||
if scoring_team:
|
||||
if scoring_team.time_remaining > 0:
|
||||
self._tick_sound.play()
|
||||
|
||||
scoring_team.time_remaining = max(
|
||||
0, scoring_team.time_remaining - 1
|
||||
)
|
||||
self._update_scoreboard()
|
||||
if scoring_team.time_remaining > 0:
|
||||
assert self._flag is not None
|
||||
self._flag.set_score_text(str(scoring_team.time_remaining))
|
||||
|
||||
# Announce numbers we have sounds for.
|
||||
numsound = self._countdownsounds.get(scoring_team.time_remaining)
|
||||
if numsound is not None:
|
||||
numsound.play()
|
||||
|
||||
# winner
|
||||
if scoring_team.time_remaining <= 0:
|
||||
self.end_game()
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = bs.GameResults()
|
||||
for team in self.teams:
|
||||
results.set_team_score(team, self._hold_time - team.time_remaining)
|
||||
self.end(results=results, announce_delay=0)
|
||||
|
||||
def _update_flag_state(self) -> None:
|
||||
holding_teams = set(
|
||||
player.team for player in self.players if player.time_at_flag
|
||||
)
|
||||
prev_state = self._flag_state
|
||||
assert self._flag_light
|
||||
assert self._flag is not None
|
||||
assert self._flag.node
|
||||
if len(holding_teams) > 1:
|
||||
self._flag_state = FlagState.CONTESTED
|
||||
self._scoring_team = None
|
||||
self._flag_light.color = (0.6, 0.6, 0.1)
|
||||
self._flag.node.color = (1.0, 1.0, 0.4)
|
||||
elif len(holding_teams) == 1:
|
||||
holding_team = list(holding_teams)[0]
|
||||
self._flag_state = FlagState.HELD
|
||||
self._scoring_team = weakref.ref(holding_team)
|
||||
self._flag_light.color = bs.normalized_color(holding_team.color)
|
||||
self._flag.node.color = holding_team.color
|
||||
else:
|
||||
self._flag_state = FlagState.UNCONTESTED
|
||||
self._scoring_team = None
|
||||
self._flag_light.color = (0.2, 0.2, 0.2)
|
||||
self._flag.node.color = (1, 1, 1)
|
||||
if self._flag_state != prev_state:
|
||||
self._swipsound.play()
|
||||
|
||||
def _handle_player_flag_region_collide(self, colliding: bool) -> None:
|
||||
try:
|
||||
spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
|
||||
except bs.NotFoundError:
|
||||
return
|
||||
|
||||
if not spaz.is_alive():
|
||||
return
|
||||
|
||||
player = spaz.getplayer(Player, True)
|
||||
|
||||
# Different parts of us can collide so a single value isn't enough
|
||||
# also don't count it if we're dead (flying heads shouldn't be able to
|
||||
# win the game :-)
|
||||
if colliding and player.is_alive():
|
||||
player.time_at_flag += 1
|
||||
else:
|
||||
player.time_at_flag = max(0, player.time_at_flag - 1)
|
||||
|
||||
self._update_flag_state()
|
||||
|
||||
def _update_scoreboard(self) -> None:
|
||||
for team in self.teams:
|
||||
self._scoreboard.set_team_value(
|
||||
team, team.time_remaining, self._hold_time, countdown=True
|
||||
)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
super().handlemessage(msg) # Augment default.
|
||||
|
||||
# No longer can count as time_at_flag once dead.
|
||||
player = msg.getplayer(Player)
|
||||
player.time_at_flag = 0
|
||||
self._update_flag_state()
|
||||
self.respawn_player(player)
|
||||
264
dist/ba_data/python/bascenev1lib/game/meteorshower.py
vendored
Normal file
264
dist/ba_data/python/bascenev1lib/game/meteorshower.py
vendored
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Defines a bomb-dodging mini-game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.bomb import Bomb
|
||||
from bascenev1lib.actor.onscreentimer import OnScreenTimer
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.death_time: float | None = None
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class MeteorShowerGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""Minigame involving dodging falling bombs."""
|
||||
|
||||
name = 'Meteor Shower'
|
||||
description = 'Dodge the falling bombs.'
|
||||
available_settings = [bs.BoolSetting('Epic Mode', default=False)]
|
||||
scoreconfig = bs.ScoreConfig(
|
||||
label='Survived', scoretype=bs.ScoreType.MILLISECONDS, version='B'
|
||||
)
|
||||
|
||||
# Print messages when players die (since its meaningful in this game).
|
||||
announce_player_deaths = True
|
||||
|
||||
# Don't allow joining after we start
|
||||
# (would enable leave/rejoin tomfoolery).
|
||||
allow_mid_activity_joins = False
|
||||
|
||||
# We're currently hard-coded for one map.
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
return ['Rampage']
|
||||
|
||||
# We support teams, free-for-all, and co-op sessions.
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
return (
|
||||
issubclass(sessiontype, bs.DualTeamSession)
|
||||
or issubclass(sessiontype, bs.FreeForAllSession)
|
||||
or issubclass(sessiontype, bs.CoopSession)
|
||||
)
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
|
||||
self._epic_mode = settings.get('Epic Mode', False)
|
||||
self._last_player_death_time: float | None = None
|
||||
self._meteor_time = 2.0
|
||||
self._timer: OnScreenTimer | None = None
|
||||
|
||||
# Some base class overrides:
|
||||
self.default_music = (
|
||||
bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL
|
||||
)
|
||||
if self._epic_mode:
|
||||
self.slow_motion = True
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
|
||||
# Drop a wave every few seconds.. and every so often drop the time
|
||||
# between waves ..lets have things increase faster if we have fewer
|
||||
# players.
|
||||
delay = 5.0 if len(self.players) > 2 else 2.5
|
||||
if self._epic_mode:
|
||||
delay *= 0.25
|
||||
bs.timer(delay, self._decrement_meteor_time, repeat=True)
|
||||
|
||||
# Kick off the first wave in a few seconds.
|
||||
delay = 3.0
|
||||
if self._epic_mode:
|
||||
delay *= 0.25
|
||||
bs.timer(delay, self._set_meteor_timer)
|
||||
|
||||
self._timer = OnScreenTimer()
|
||||
self._timer.start()
|
||||
|
||||
# Check for immediate end (if we've only got 1 player, etc).
|
||||
bs.timer(5.0, self._check_end_game)
|
||||
|
||||
def on_player_leave(self, player: Player) -> None:
|
||||
# Augment default behavior.
|
||||
super().on_player_leave(player)
|
||||
|
||||
# A departing player may trigger game-over.
|
||||
self._check_end_game()
|
||||
|
||||
# overriding the default character spawning..
|
||||
def spawn_player(self, player: Player) -> bs.Actor:
|
||||
spaz = self.spawn_player_spaz(player)
|
||||
|
||||
# Let's reconnect this player's controls to this
|
||||
# spaz but *without* the ability to attack or pick stuff up.
|
||||
spaz.connect_controls_to_player(
|
||||
enable_punch=False, enable_bomb=False, enable_pickup=False
|
||||
)
|
||||
|
||||
# Also lets have them make some noise when they die.
|
||||
spaz.play_big_death_sound = True
|
||||
return spaz
|
||||
|
||||
# Various high-level game events come through this method.
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
|
||||
curtime = bs.time()
|
||||
|
||||
# Record the player's moment of death.
|
||||
# assert isinstance(msg.spaz.player
|
||||
msg.getplayer(Player).death_time = curtime
|
||||
|
||||
# In co-op mode, end the game the instant everyone dies
|
||||
# (more accurate looking).
|
||||
# In teams/ffa, allow a one-second fudge-factor so we can
|
||||
# get more draws if players die basically at the same time.
|
||||
if isinstance(self.session, bs.CoopSession):
|
||||
# Teams will still show up if we check now.. check in
|
||||
# the next cycle.
|
||||
bs.pushcall(self._check_end_game)
|
||||
|
||||
# Also record this for a final setting of the clock.
|
||||
self._last_player_death_time = curtime
|
||||
else:
|
||||
bs.timer(1.0, self._check_end_game)
|
||||
|
||||
else:
|
||||
# Default handler:
|
||||
return super().handlemessage(msg)
|
||||
return None
|
||||
|
||||
def _check_end_game(self) -> None:
|
||||
living_team_count = 0
|
||||
for team in self.teams:
|
||||
for player in team.players:
|
||||
if player.is_alive():
|
||||
living_team_count += 1
|
||||
break
|
||||
|
||||
# In co-op, we go till everyone is dead.. otherwise we go
|
||||
# until one team remains.
|
||||
if isinstance(self.session, bs.CoopSession):
|
||||
if living_team_count <= 0:
|
||||
self.end_game()
|
||||
else:
|
||||
if living_team_count <= 1:
|
||||
self.end_game()
|
||||
|
||||
def _set_meteor_timer(self) -> None:
|
||||
bs.timer(
|
||||
(1.0 + 0.2 * random.random()) * self._meteor_time,
|
||||
self._drop_bomb_cluster,
|
||||
)
|
||||
|
||||
def _drop_bomb_cluster(self) -> None:
|
||||
# Random note: code like this is a handy way to plot out extents
|
||||
# and debug things.
|
||||
loc_test = False
|
||||
if loc_test:
|
||||
bs.newnode('locator', attrs={'position': (8, 6, -5.5)})
|
||||
bs.newnode('locator', attrs={'position': (8, 6, -2.3)})
|
||||
bs.newnode('locator', attrs={'position': (-7.3, 6, -5.5)})
|
||||
bs.newnode('locator', attrs={'position': (-7.3, 6, -2.3)})
|
||||
|
||||
# Drop several bombs in series.
|
||||
delay = 0.0
|
||||
for _i in range(random.randrange(1, 3)):
|
||||
# Drop them somewhere within our bounds with velocity pointing
|
||||
# toward the opposite side.
|
||||
pos = (
|
||||
-7.3 + 15.3 * random.random(),
|
||||
11,
|
||||
-5.57 + 2.1 * random.random(),
|
||||
)
|
||||
dropdir = -1.0 if pos[0] > 0 else 1.0
|
||||
vel = (
|
||||
(-5.0 + random.random() * 30.0) * dropdir,
|
||||
random.uniform(-3.066, -4.12),
|
||||
0,
|
||||
)
|
||||
bs.timer(delay, bs.Call(self._drop_bomb, pos, vel))
|
||||
delay += 0.1
|
||||
self._set_meteor_timer()
|
||||
|
||||
def _drop_bomb(
|
||||
self, position: Sequence[float], velocity: Sequence[float]
|
||||
) -> None:
|
||||
Bomb(position=position, velocity=velocity).autoretain()
|
||||
|
||||
def _decrement_meteor_time(self) -> None:
|
||||
self._meteor_time = max(0.01, self._meteor_time * 0.9)
|
||||
|
||||
def end_game(self) -> None:
|
||||
cur_time = bs.time()
|
||||
assert self._timer is not None
|
||||
start_time = self._timer.getstarttime()
|
||||
|
||||
# Mark death-time as now for any still-living players
|
||||
# and award players points for how long they lasted.
|
||||
# (these per-player scores are only meaningful in team-games)
|
||||
for team in self.teams:
|
||||
for player in team.players:
|
||||
survived = False
|
||||
|
||||
# Throw an extra fudge factor in so teams that
|
||||
# didn't die come out ahead of teams that did.
|
||||
if player.death_time is None:
|
||||
survived = True
|
||||
player.death_time = cur_time + 1
|
||||
|
||||
# Award a per-player score depending on how many seconds
|
||||
# they lasted (per-player scores only affect teams mode;
|
||||
# everywhere else just looks at the per-team score).
|
||||
score = int(player.death_time - self._timer.getstarttime())
|
||||
if survived:
|
||||
score += 50 # A bit extra for survivors.
|
||||
self.stats.player_scored(player, score, screenmessage=False)
|
||||
|
||||
# Stop updating our time text, and set the final time to match
|
||||
# exactly when our last guy died.
|
||||
self._timer.stop(endtime=self._last_player_death_time)
|
||||
|
||||
# Ok now calc game results: set a score for each team and then tell
|
||||
# the game to end.
|
||||
results = bs.GameResults()
|
||||
|
||||
# Remember that 'free-for-all' mode is simply a special form
|
||||
# of 'teams' mode where each player gets their own team, so we can
|
||||
# just always deal in teams and have all cases covered.
|
||||
for team in self.teams:
|
||||
# Set the team score to the max time survived by any player on
|
||||
# that team.
|
||||
longest_life = 0.0
|
||||
for player in team.players:
|
||||
assert player.death_time is not None
|
||||
longest_life = max(longest_life, player.death_time - start_time)
|
||||
|
||||
# Submit the score value in milliseconds.
|
||||
results.set_team_score(team, int(1000.0 * longest_life))
|
||||
|
||||
self.end(results=results)
|
||||
191
dist/ba_data/python/bascenev1lib/game/ninjafight.py
vendored
Normal file
191
dist/ba_data/python/bascenev1lib/game/ninjafight.py
vendored
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides Ninja Fight mini-game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.spazbot import (
|
||||
SpazBotSet,
|
||||
ChargerBot,
|
||||
SpazBotDiedMessage,
|
||||
)
|
||||
from bascenev1lib.actor.onscreentimer import OnScreenTimer
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class NinjaFightGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""
|
||||
A co-op game where you try to defeat a group
|
||||
of Ninjas as fast as possible
|
||||
"""
|
||||
|
||||
name = 'Ninja Fight'
|
||||
description = 'How fast can you defeat the ninjas?'
|
||||
scoreconfig = bs.ScoreConfig(
|
||||
label='Time', scoretype=bs.ScoreType.MILLISECONDS, lower_is_better=True
|
||||
)
|
||||
default_music = bs.MusicType.TO_THE_DEATH
|
||||
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
# For now we're hard-coding spawn positions and whatnot
|
||||
# so we need to be sure to specify that we only support
|
||||
# a specific map.
|
||||
return ['Courtyard']
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
# We currently support Co-Op only.
|
||||
return issubclass(sessiontype, bs.CoopSession)
|
||||
|
||||
# In the constructor we should load any media we need/etc.
|
||||
# ...but not actually create anything yet.
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
self._winsound = bs.getsound('score')
|
||||
self._won = False
|
||||
self._timer: OnScreenTimer | None = None
|
||||
self._bots = SpazBotSet()
|
||||
self._preset = str(settings['preset'])
|
||||
|
||||
# Called when our game actually begins.
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
is_pro = self._preset == 'pro'
|
||||
|
||||
# In pro mode there's no powerups.
|
||||
if not is_pro:
|
||||
self.setup_standard_powerup_drops()
|
||||
|
||||
# Make our on-screen timer and start it roughly when our bots appear.
|
||||
self._timer = OnScreenTimer()
|
||||
bs.timer(4.0, self._timer.start)
|
||||
|
||||
# Spawn some baddies.
|
||||
bs.timer(
|
||||
1.0,
|
||||
lambda: self._bots.spawn_bot(
|
||||
ChargerBot, pos=(3, 3, -2), spawn_time=3.0
|
||||
),
|
||||
)
|
||||
bs.timer(
|
||||
2.0,
|
||||
lambda: self._bots.spawn_bot(
|
||||
ChargerBot, pos=(-3, 3, -2), spawn_time=3.0
|
||||
),
|
||||
)
|
||||
bs.timer(
|
||||
3.0,
|
||||
lambda: self._bots.spawn_bot(
|
||||
ChargerBot, pos=(5, 3, -2), spawn_time=3.0
|
||||
),
|
||||
)
|
||||
bs.timer(
|
||||
4.0,
|
||||
lambda: self._bots.spawn_bot(
|
||||
ChargerBot, pos=(-5, 3, -2), spawn_time=3.0
|
||||
),
|
||||
)
|
||||
|
||||
# Add some extras for multiplayer or pro mode.
|
||||
assert self.initialplayerinfos is not None
|
||||
if len(self.initialplayerinfos) > 2 or is_pro:
|
||||
bs.timer(
|
||||
5.0,
|
||||
lambda: self._bots.spawn_bot(
|
||||
ChargerBot, pos=(0, 3, -5), spawn_time=3.0
|
||||
),
|
||||
)
|
||||
if len(self.initialplayerinfos) > 3 or is_pro:
|
||||
bs.timer(
|
||||
6.0,
|
||||
lambda: self._bots.spawn_bot(
|
||||
ChargerBot, pos=(0, 3, 1), spawn_time=3.0
|
||||
),
|
||||
)
|
||||
|
||||
# Called for each spawning player.
|
||||
def spawn_player(self, player: Player) -> bs.Actor:
|
||||
# Let's spawn close to the center.
|
||||
spawn_center = (0, 3, -2)
|
||||
pos = (
|
||||
spawn_center[0] + random.uniform(-1.5, 1.5),
|
||||
spawn_center[1],
|
||||
spawn_center[2] + random.uniform(-1.5, 1.5),
|
||||
)
|
||||
return self.spawn_player_spaz(player, position=pos)
|
||||
|
||||
def _check_if_won(self) -> None:
|
||||
# Simply end the game if there's no living bots.
|
||||
# FIXME: Should also make sure all bots have been spawned;
|
||||
# if spawning is spread out enough that we're able to kill
|
||||
# all living bots before the next spawns, it would incorrectly
|
||||
# count as a win.
|
||||
if not self._bots.have_living_bots():
|
||||
self._won = True
|
||||
self.end_game()
|
||||
|
||||
# Called for miscellaneous messages.
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
# A player has died.
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
super().handlemessage(msg) # Augment standard behavior.
|
||||
self.respawn_player(msg.getplayer(Player))
|
||||
|
||||
# A spaz-bot has died.
|
||||
elif isinstance(msg, SpazBotDiedMessage):
|
||||
# Unfortunately the bot-set will always tell us there are living
|
||||
# bots if we ask here (the currently-dying bot isn't officially
|
||||
# marked dead yet) ..so lets push a call into the event loop to
|
||||
# check once this guy has finished dying.
|
||||
bs.pushcall(self._check_if_won)
|
||||
|
||||
# Let the base class handle anything we don't.
|
||||
else:
|
||||
return super().handlemessage(msg)
|
||||
return None
|
||||
|
||||
# When this is called, we should fill out results and end the game
|
||||
# *regardless* of whether is has been won. (this may be called due
|
||||
# to a tournament ending or other external reason).
|
||||
def end_game(self) -> None:
|
||||
# Stop our on-screen timer so players can see what they got.
|
||||
assert self._timer is not None
|
||||
self._timer.stop()
|
||||
|
||||
results = bs.GameResults()
|
||||
|
||||
# If we won, set our score to the elapsed time in milliseconds.
|
||||
# (there should just be 1 team here since this is co-op).
|
||||
# ..if we didn't win, leave scores as default (None) which means
|
||||
# we lost.
|
||||
if self._won:
|
||||
elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0)
|
||||
bs.cameraflash()
|
||||
self._winsound.play()
|
||||
for team in self.teams:
|
||||
for player in team.players:
|
||||
if player.actor:
|
||||
player.actor.handlemessage(bs.CelebrateMessage())
|
||||
results.set_team_score(team, elapsed_time_ms)
|
||||
|
||||
# Ends the activity.
|
||||
self.end(results)
|
||||
1550
dist/ba_data/python/bascenev1lib/game/onslaught.py
vendored
Normal file
1550
dist/ba_data/python/bascenev1lib/game/onslaught.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
798
dist/ba_data/python/bascenev1lib/game/race.py
vendored
Normal file
798
dist/ba_data/python/bascenev1lib/game/race.py
vendored
Normal file
|
|
@ -0,0 +1,798 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Defines Race mini-game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bascenev1lib.actor.bomb import Bomb
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.gameutils import SharedObjects
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
from bascenev1lib.actor.onscreentimer import OnScreenTimer
|
||||
|
||||
|
||||
@dataclass
|
||||
class RaceMine:
|
||||
"""Holds info about a mine on the track."""
|
||||
|
||||
point: Sequence[float]
|
||||
mine: Bomb | None
|
||||
|
||||
|
||||
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],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.distance_txt: bs.Node | None = None
|
||||
self.last_region = 0
|
||||
self.lap = 0
|
||||
self.distance = 0.0
|
||||
self.finished = False
|
||||
self.rank: int | None = None
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.time: float | None = 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 = 'Race'
|
||||
description = 'Run real fast!'
|
||||
scoreconfig = bs.ScoreConfig(
|
||||
label='Time', lower_is_better=True, scoretype=bs.ScoreType.MILLISECONDS
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_available_settings(
|
||||
cls, sessiontype: type[bs.Session]
|
||||
) -> list[bs.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.IntChoiceSetting(
|
||||
'Mine Spawning',
|
||||
default=4000,
|
||||
choices=[
|
||||
('No Mines', 0),
|
||||
('8 Seconds', 8000),
|
||||
('4 Seconds', 4000),
|
||||
('2 Seconds', 2000),
|
||||
],
|
||||
),
|
||||
bs.IntChoiceSetting(
|
||||
'Bomb Spawning',
|
||||
choices=[
|
||||
('None', 0),
|
||||
('8 Seconds', 8000),
|
||||
('4 Seconds', 4000),
|
||||
('2 Seconds', 2000),
|
||||
('1 Second', 1000),
|
||||
],
|
||||
default=2000,
|
||||
),
|
||||
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]:
|
||||
assert bs.app.classic is not None
|
||||
return bs.app.classic.getmaps('race')
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
self._race_started = False
|
||||
super().__init__(settings)
|
||||
self._scoreboard = Scoreboard()
|
||||
self._score_sound = bs.getsound('score')
|
||||
self._swipsound = bs.getsound('swip')
|
||||
self._last_team_time: float | None = None
|
||||
self._front_race_region: int | None = 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: bs.Material | None = None
|
||||
self._regions: list[RaceRegion] = []
|
||||
self._team_finish_pts: int | None = None
|
||||
self._time_text: bs.Actor | None = None
|
||||
self._timer: OnScreenTimer | None = None
|
||||
self._race_mines: list[RaceMine] | None = None
|
||||
self._race_mine_timer: bs.Timer | None = None
|
||||
self._scoreboard_timer: bs.Timer | None = None
|
||||
self._player_order_update_timer: bs.Timer | None = None
|
||||
self._start_lights: list[bs.Node] | None = None
|
||||
self._bomb_spawn_timer: bs.Timer | None = 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._mine_spawning = int(settings['Mine Spawning'])
|
||||
self._bomb_spawning = int(settings['Bomb Spawning'])
|
||||
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
|
||||
)
|
||||
|
||||
def get_instance_description(self) -> 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) -> 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)))
|
||||
|
||||
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(
|
||||
bs.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 = bs.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:
|
||||
logging.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(
|
||||
bs.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:
|
||||
logging.exception('Error sending DieMessage.')
|
||||
|
||||
# Defer so team/player lists will be updated.
|
||||
bs.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()
|
||||
|
||||
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:
|
||||
self._race_mine_timer = bs.Timer(
|
||||
0.001 * self._mine_spawning,
|
||||
self._update_race_mine,
|
||||
repeat=True,
|
||||
)
|
||||
|
||||
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._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)
|
||||
|
||||
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:
|
||||
logging.exception('Error in race player connects.')
|
||||
assert self._timer is not None
|
||||
self._timer.start()
|
||||
|
||||
if self._bomb_spawning != 0:
|
||||
self._bomb_spawn_timer = bs.Timer(
|
||||
0.001 * self._bomb_spawning, self._spawn_bomb, repeat=True
|
||||
)
|
||||
|
||||
self._race_started = True
|
||||
|
||||
def _update_player_order(self) -> None:
|
||||
# Calc all player distances.
|
||||
for player in self.players:
|
||||
pos: bs.Vec3 | None
|
||||
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 = bs.Vec3(rg1.pos[:3])
|
||||
rg2 = (
|
||||
self._regions[0]
|
||||
if r_index == len(self._regions) - 1
|
||||
else self._regions[r_index + 1]
|
||||
)
|
||||
r2pt = bs.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_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),
|
||||
)
|
||||
bs.timer(
|
||||
random.uniform(0.0, 2.0), bs.WeakCall(self._spawn_bomb_at_pos, pos)
|
||||
)
|
||||
|
||||
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]
|
||||
light = bs.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': rmine.point[:3],
|
||||
'color': (1, 0.2, 0.2),
|
||||
'radius': 0.1,
|
||||
'height_attenuated': False,
|
||||
},
|
||||
)
|
||||
bs.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True)
|
||||
bs.timer(1.0, light.delete)
|
||||
|
||||
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)
|
||||
bs.timer(0.95, bs.Call(self._make_mine, m_index))
|
||||
|
||||
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)
|
||||
1360
dist/ba_data/python/bascenev1lib/game/runaround.py
vendored
Normal file
1360
dist/ba_data/python/bascenev1lib/game/runaround.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
384
dist/ba_data/python/bascenev1lib/game/targetpractice.py
vendored
Normal file
384
dist/ba_data/python/bascenev1lib/game/targetpractice.py
vendored
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Implements Target Practice game."""
|
||||
|
||||
# ba_meta require api 8
|
||||
# (see https://ballistica.net/wiki/meta-tag-system)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.actor.onscreencountdown import OnScreenCountdown
|
||||
from bascenev1lib.actor.bomb import Bomb
|
||||
from bascenev1lib.actor.popuptext import PopupText
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
||||
from bascenev1lib.actor.bomb import Blast
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.streak = 0
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.score = 0
|
||||
|
||||
|
||||
# ba_meta export bascenev1.GameActivity
|
||||
class TargetPracticeGame(bs.TeamGameActivity[Player, Team]):
|
||||
"""Game where players try to hit targets with bombs."""
|
||||
|
||||
name = 'Target Practice'
|
||||
description = 'Bomb as many targets as you can.'
|
||||
available_settings = [
|
||||
bs.IntSetting('Target Count', min_value=1, default=3),
|
||||
bs.BoolSetting('Enable Impact Bombs', default=True),
|
||||
bs.BoolSetting('Enable Triple Bombs', default=True),
|
||||
]
|
||||
default_music = bs.MusicType.FORWARD_MARCH
|
||||
|
||||
@classmethod
|
||||
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
|
||||
return ['Doom Shroom']
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
|
||||
# We support any teams or versus sessions.
|
||||
return issubclass(sessiontype, bs.CoopSession) or issubclass(
|
||||
sessiontype, bs.MultiTeamSession
|
||||
)
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
self._scoreboard = Scoreboard()
|
||||
self._targets: list[Target] = []
|
||||
self._update_timer: bs.Timer | None = None
|
||||
self._countdown: OnScreenCountdown | None = None
|
||||
self._target_count = int(settings['Target Count'])
|
||||
self._enable_impact_bombs = bool(settings['Enable Impact Bombs'])
|
||||
self._enable_triple_bombs = bool(settings['Enable Triple Bombs'])
|
||||
|
||||
def on_team_join(self, team: Team) -> None:
|
||||
if self.has_begun():
|
||||
self.update_scoreboard()
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
self.update_scoreboard()
|
||||
|
||||
# Number of targets is based on player count.
|
||||
for i in range(self._target_count):
|
||||
bs.timer(5.0 + i * 1.0, self._spawn_target)
|
||||
|
||||
self._update_timer = bs.Timer(1.0, self._update, repeat=True)
|
||||
self._countdown = OnScreenCountdown(60, endcall=self.end_game)
|
||||
bs.timer(4.0, self._countdown.start)
|
||||
|
||||
def spawn_player(self, player: Player) -> bs.Actor:
|
||||
spawn_center = (0, 3, -5)
|
||||
pos = (
|
||||
spawn_center[0] + random.uniform(-1.5, 1.5),
|
||||
spawn_center[1],
|
||||
spawn_center[2] + random.uniform(-1.5, 1.5),
|
||||
)
|
||||
|
||||
# Reset their streak.
|
||||
player.streak = 0
|
||||
spaz = self.spawn_player_spaz(player, position=pos)
|
||||
|
||||
# Give players permanent triple impact bombs and wire them up
|
||||
# to tell us when they drop a bomb.
|
||||
if self._enable_impact_bombs:
|
||||
spaz.bomb_type = 'impact'
|
||||
if self._enable_triple_bombs:
|
||||
spaz.set_bomb_count(3)
|
||||
spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb)
|
||||
return spaz
|
||||
|
||||
def _spawn_target(self) -> None:
|
||||
# Generate a few random points; we'll use whichever one is farthest
|
||||
# from our existing targets (don't want overlapping targets).
|
||||
points = []
|
||||
|
||||
for _i in range(4):
|
||||
# Calc a random point within a circle.
|
||||
while True:
|
||||
xpos = random.uniform(-1.0, 1.0)
|
||||
ypos = random.uniform(-1.0, 1.0)
|
||||
if xpos * xpos + ypos * ypos < 1.0:
|
||||
break
|
||||
points.append(bs.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos))
|
||||
|
||||
def get_min_dist_from_target(pnt: bs.Vec3) -> float:
|
||||
return min((t.get_dist_from_point(pnt) for t in self._targets))
|
||||
|
||||
# If we have existing targets, use the point with the highest
|
||||
# min-distance-from-targets.
|
||||
if self._targets:
|
||||
point = max(points, key=get_min_dist_from_target)
|
||||
else:
|
||||
point = points[0]
|
||||
|
||||
self._targets.append(Target(position=point))
|
||||
|
||||
def _on_spaz_dropped_bomb(self, spaz: bs.Actor, bomb: bs.Actor) -> None:
|
||||
del spaz # Unused.
|
||||
|
||||
# Wire up this bomb to inform us when it blows up.
|
||||
assert isinstance(bomb, Bomb)
|
||||
bomb.add_explode_callback(self._on_bomb_exploded)
|
||||
|
||||
def _on_bomb_exploded(self, bomb: Bomb, blast: Blast) -> None:
|
||||
assert blast.node
|
||||
pos = blast.node.position
|
||||
|
||||
# Debugging: throw a locator down where we landed.
|
||||
# bs.newnode('locator', attrs={'position':blast.node.position})
|
||||
|
||||
# Feed the explosion point to all our targets and get points in return.
|
||||
# Note: we operate on a copy of self._targets since the list may change
|
||||
# under us if we hit stuff (don't wanna get points for new targets).
|
||||
player = bomb.get_source_player(Player)
|
||||
if not player:
|
||||
# It's possible the player left after throwing the bomb.
|
||||
return
|
||||
|
||||
bullseye = any(
|
||||
target.do_hit_at_position(pos, player)
|
||||
for target in list(self._targets)
|
||||
)
|
||||
if bullseye:
|
||||
player.streak += 1
|
||||
else:
|
||||
player.streak = 0
|
||||
|
||||
def _update(self) -> None:
|
||||
"""Misc. periodic updating."""
|
||||
# Clear out targets that have died.
|
||||
self._targets = [t for t in self._targets if t]
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
# When players die, respawn them.
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
super().handlemessage(msg) # Do standard stuff.
|
||||
player = msg.getplayer(Player)
|
||||
assert player is not None
|
||||
self.respawn_player(player) # Kick off a respawn.
|
||||
elif isinstance(msg, Target.TargetHitMessage):
|
||||
# A target is telling us it was hit and will die soon..
|
||||
# ..so make another one.
|
||||
self._spawn_target()
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
|
||||
def update_scoreboard(self) -> None:
|
||||
"""Update the game scoreboard with current team values."""
|
||||
for team in self.teams:
|
||||
self._scoreboard.set_team_value(team, team.score)
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = bs.GameResults()
|
||||
for team in self.teams:
|
||||
results.set_team_score(team, team.score)
|
||||
self.end(results)
|
||||
|
||||
|
||||
class Target(bs.Actor):
|
||||
"""A target practice target."""
|
||||
|
||||
class TargetHitMessage:
|
||||
"""Inform an object a target was hit."""
|
||||
|
||||
def __init__(self, position: Sequence[float]):
|
||||
self._r1 = 0.45
|
||||
self._r2 = 1.1
|
||||
self._r3 = 2.0
|
||||
self._rfudge = 0.15
|
||||
super().__init__()
|
||||
self._position = bs.Vec3(position)
|
||||
self._hit = False
|
||||
|
||||
# It can be handy to test with this on to make sure the projection
|
||||
# isn't too far off from the actual object.
|
||||
show_in_space = False
|
||||
loc1 = bs.newnode(
|
||||
'locator',
|
||||
attrs={
|
||||
'shape': 'circle',
|
||||
'position': position,
|
||||
'color': (0, 1, 0),
|
||||
'opacity': 0.5,
|
||||
'draw_beauty': show_in_space,
|
||||
'additive': True,
|
||||
},
|
||||
)
|
||||
loc2 = bs.newnode(
|
||||
'locator',
|
||||
attrs={
|
||||
'shape': 'circleOutline',
|
||||
'position': position,
|
||||
'color': (0, 1, 0),
|
||||
'opacity': 0.3,
|
||||
'draw_beauty': False,
|
||||
'additive': True,
|
||||
},
|
||||
)
|
||||
loc3 = bs.newnode(
|
||||
'locator',
|
||||
attrs={
|
||||
'shape': 'circleOutline',
|
||||
'position': position,
|
||||
'color': (0, 1, 0),
|
||||
'opacity': 0.1,
|
||||
'draw_beauty': False,
|
||||
'additive': True,
|
||||
},
|
||||
)
|
||||
self._nodes = [loc1, loc2, loc3]
|
||||
bs.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]})
|
||||
bs.animate_array(loc2, 'size', 1, {0.05: [0.0], 0.25: [self._r2 * 2.0]})
|
||||
bs.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]})
|
||||
bs.getsound('laserReverse').play()
|
||||
|
||||
def exists(self) -> bool:
|
||||
return bool(self._nodes)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.DieMessage):
|
||||
for node in self._nodes:
|
||||
node.delete()
|
||||
self._nodes = []
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
|
||||
def get_dist_from_point(self, pos: bs.Vec3) -> float:
|
||||
"""Given a point, returns distance squared from it."""
|
||||
return (pos - self._position).length()
|
||||
|
||||
def do_hit_at_position(self, pos: Sequence[float], player: Player) -> bool:
|
||||
"""Handle a bomb hit at the given position."""
|
||||
# pylint: disable=too-many-statements
|
||||
activity = self.activity
|
||||
|
||||
# Ignore hits if the game is over or if we've already been hit
|
||||
if activity.has_ended() or self._hit or not self._nodes:
|
||||
return False
|
||||
|
||||
diff = bs.Vec3(pos) - self._position
|
||||
|
||||
# Disregard Y difference. Our target point probably isn't exactly
|
||||
# on the ground anyway.
|
||||
diff[1] = 0.0
|
||||
dist = diff.length()
|
||||
|
||||
bullseye = False
|
||||
if dist <= self._r3 + self._rfudge:
|
||||
# Inform our activity that we were hit
|
||||
self._hit = True
|
||||
activity.handlemessage(self.TargetHitMessage())
|
||||
keys: dict[float, Sequence[float]] = {
|
||||
0.0: (1.0, 0.0, 0.0),
|
||||
0.049: (1.0, 0.0, 0.0),
|
||||
0.05: (1.0, 1.0, 1.0),
|
||||
0.1: (0.0, 1.0, 0.0),
|
||||
}
|
||||
cdull = (0.3, 0.3, 0.3)
|
||||
popupcolor: Sequence[float]
|
||||
if dist <= self._r1 + self._rfudge:
|
||||
bullseye = True
|
||||
self._nodes[1].color = cdull
|
||||
self._nodes[2].color = cdull
|
||||
bs.animate_array(self._nodes[0], 'color', 3, keys, loop=True)
|
||||
popupscale = 1.8
|
||||
popupcolor = (1, 1, 0, 1)
|
||||
streak = player.streak
|
||||
points = 10 + min(20, streak * 2)
|
||||
bs.getsound('bellHigh').play()
|
||||
if streak > 0:
|
||||
bs.getsound(
|
||||
'orchestraHit4'
|
||||
if streak > 3
|
||||
else 'orchestraHit3'
|
||||
if streak > 2
|
||||
else 'orchestraHit2'
|
||||
if streak > 1
|
||||
else 'orchestraHit'
|
||||
).play()
|
||||
elif dist <= self._r2 + self._rfudge:
|
||||
self._nodes[0].color = cdull
|
||||
self._nodes[2].color = cdull
|
||||
bs.animate_array(self._nodes[1], 'color', 3, keys, loop=True)
|
||||
popupscale = 1.25
|
||||
popupcolor = (1, 0.5, 0.2, 1)
|
||||
points = 4
|
||||
bs.getsound('bellMed').play()
|
||||
else:
|
||||
self._nodes[0].color = cdull
|
||||
self._nodes[1].color = cdull
|
||||
bs.animate_array(self._nodes[2], 'color', 3, keys, loop=True)
|
||||
popupscale = 1.0
|
||||
popupcolor = (0.8, 0.3, 0.3, 1)
|
||||
points = 2
|
||||
bs.getsound('bellLow').play()
|
||||
|
||||
# Award points/etc.. (technically should probably leave this up
|
||||
# to the activity).
|
||||
popupstr = '+' + str(points)
|
||||
|
||||
# If there's more than 1 player in the game, include their
|
||||
# names and colors so they know who got the hit.
|
||||
if len(activity.players) > 1:
|
||||
popupcolor = bs.safecolor(player.color, target_intensity=0.75)
|
||||
popupstr += ' ' + player.getname()
|
||||
PopupText(
|
||||
popupstr,
|
||||
position=self._position,
|
||||
color=popupcolor,
|
||||
scale=popupscale,
|
||||
).autoretain()
|
||||
|
||||
# Give this player's team points and update the score-board.
|
||||
player.team.score += points
|
||||
assert isinstance(activity, TargetPracticeGame)
|
||||
activity.update_scoreboard()
|
||||
|
||||
# Also give this individual player points
|
||||
# (only applies in teams mode).
|
||||
assert activity.stats is not None
|
||||
activity.stats.player_scored(
|
||||
player, points, showpoints=False, screenmessage=False
|
||||
)
|
||||
|
||||
bs.animate_array(
|
||||
self._nodes[0],
|
||||
'size',
|
||||
1,
|
||||
{0.8: self._nodes[0].size, 1.0: [0.0]},
|
||||
)
|
||||
bs.animate_array(
|
||||
self._nodes[1],
|
||||
'size',
|
||||
1,
|
||||
{0.85: self._nodes[1].size, 1.05: [0.0]},
|
||||
)
|
||||
bs.animate_array(
|
||||
self._nodes[2],
|
||||
'size',
|
||||
1,
|
||||
{0.9: self._nodes[2].size, 1.1: [0.0]},
|
||||
)
|
||||
bs.timer(1.1, bs.Call(self.handlemessage, bs.DieMessage()))
|
||||
|
||||
return bullseye
|
||||
339
dist/ba_data/python/bascenev1lib/game/thelaststand.py
vendored
Normal file
339
dist/ba_data/python/bascenev1lib/game/thelaststand.py
vendored
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Defines the last stand minigame."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
from bascenev1lib.actor.bomb import TNTSpawner
|
||||
from bascenev1lib.actor.scoreboard import Scoreboard
|
||||
from bascenev1lib.actor.powerupbox import PowerupBoxFactory, PowerupBox
|
||||
from bascenev1lib.actor.spazbot import (
|
||||
SpazBotSet,
|
||||
SpazBotDiedMessage,
|
||||
BomberBot,
|
||||
BomberBotPro,
|
||||
BomberBotProShielded,
|
||||
BrawlerBot,
|
||||
BrawlerBotPro,
|
||||
BrawlerBotProShielded,
|
||||
TriggerBot,
|
||||
TriggerBotPro,
|
||||
TriggerBotProShielded,
|
||||
ChargerBot,
|
||||
StickyBot,
|
||||
ExplodeyBot,
|
||||
)
|
||||
import bascenev1 as bs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
from bascenev1lib.actor.spazbot import SpazBot
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpawnInfo:
|
||||
"""Spawning info for a particular bot type."""
|
||||
|
||||
spawnrate: float
|
||||
increase: float
|
||||
dincrease: float
|
||||
|
||||
|
||||
class Player(bs.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
|
||||
class Team(bs.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
|
||||
class TheLastStandGame(bs.CoopGameActivity[Player, Team]):
|
||||
"""Slow motion how-long-can-you-last game."""
|
||||
|
||||
name = 'The Last Stand'
|
||||
description = 'Final glorious epic slow motion battle to the death.'
|
||||
tips = [
|
||||
'This level never ends, but a high score here\n'
|
||||
'will earn you eternal respect throughout the world.'
|
||||
]
|
||||
|
||||
# Show messages when players die since it matters here.
|
||||
announce_player_deaths = True
|
||||
|
||||
# And of course the most important part.
|
||||
slow_motion = True
|
||||
|
||||
default_music = bs.MusicType.EPIC
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
settings['map'] = 'Rampage'
|
||||
super().__init__(settings)
|
||||
self._new_wave_sound = bs.getsound('scoreHit01')
|
||||
self._winsound = bs.getsound('score')
|
||||
self._cashregistersound = bs.getsound('cashRegister')
|
||||
self._spawn_center = (0, 5.5, -4.14)
|
||||
self._tntspawnpos = (0, 5.5, -6)
|
||||
self._powerup_center = (0, 7, -4.14)
|
||||
self._powerup_spread = (7, 2)
|
||||
self._preset = str(settings.get('preset', 'default'))
|
||||
self._excludepowerups: list[str] = []
|
||||
self._scoreboard: Scoreboard | None = None
|
||||
self._score = 0
|
||||
self._bots = SpazBotSet()
|
||||
self._dingsound = bs.getsound('dingSmall')
|
||||
self._dingsoundhigh = bs.getsound('dingSmallHigh')
|
||||
self._tntspawner: TNTSpawner | None = None
|
||||
self._bot_update_interval: float | None = None
|
||||
self._bot_update_timer: bs.Timer | None = None
|
||||
self._powerup_drop_timer = None
|
||||
|
||||
# For each bot type: [spawnrate, increase, d_increase]
|
||||
self._bot_spawn_types = {
|
||||
BomberBot: SpawnInfo(1.00, 0.00, 0.000),
|
||||
BomberBotPro: SpawnInfo(0.00, 0.05, 0.001),
|
||||
BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
|
||||
BrawlerBot: SpawnInfo(1.00, 0.00, 0.000),
|
||||
BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001),
|
||||
BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
|
||||
TriggerBot: SpawnInfo(0.30, 0.00, 0.000),
|
||||
TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001),
|
||||
TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
|
||||
ChargerBot: SpawnInfo(0.30, 0.05, 0.000),
|
||||
StickyBot: SpawnInfo(0.10, 0.03, 0.001),
|
||||
ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002),
|
||||
}
|
||||
|
||||
def on_transition_in(self) -> None:
|
||||
super().on_transition_in()
|
||||
bs.timer(1.3, self._new_wave_sound.play)
|
||||
self._scoreboard = Scoreboard(
|
||||
label=bs.Lstr(resource='scoreText'), score_split=0.5
|
||||
)
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
|
||||
# Spit out a few powerups and start dropping more shortly.
|
||||
self._drop_powerups(standard_points=True)
|
||||
bs.timer(2.0, bs.WeakCall(self._start_powerup_drops))
|
||||
bs.timer(0.001, bs.WeakCall(self._start_bot_updates))
|
||||
self.setup_low_life_warning_sound()
|
||||
self._update_scores()
|
||||
self._tntspawner = TNTSpawner(
|
||||
position=self._tntspawnpos, respawn_time=10.0
|
||||
)
|
||||
|
||||
def spawn_player(self, player: Player) -> bs.Actor:
|
||||
pos = (
|
||||
self._spawn_center[0] + random.uniform(-1.5, 1.5),
|
||||
self._spawn_center[1],
|
||||
self._spawn_center[2] + random.uniform(-1.5, 1.5),
|
||||
)
|
||||
return self.spawn_player_spaz(player, position=pos)
|
||||
|
||||
def _start_bot_updates(self) -> None:
|
||||
self._bot_update_interval = 3.3 - 0.3 * (len(self.players))
|
||||
self._update_bots()
|
||||
self._update_bots()
|
||||
if len(self.players) > 2:
|
||||
self._update_bots()
|
||||
if len(self.players) > 3:
|
||||
self._update_bots()
|
||||
self._bot_update_timer = bs.Timer(
|
||||
self._bot_update_interval, bs.WeakCall(self._update_bots)
|
||||
)
|
||||
|
||||
def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None:
|
||||
if poweruptype is None:
|
||||
poweruptype = PowerupBoxFactory.get().get_random_powerup_type(
|
||||
excludetypes=self._excludepowerups
|
||||
)
|
||||
PowerupBox(
|
||||
position=self.map.powerup_spawn_points[index],
|
||||
poweruptype=poweruptype,
|
||||
).autoretain()
|
||||
|
||||
def _start_powerup_drops(self) -> None:
|
||||
self._powerup_drop_timer = bs.Timer(
|
||||
3.0, bs.WeakCall(self._drop_powerups), repeat=True
|
||||
)
|
||||
|
||||
def _drop_powerups(
|
||||
self, standard_points: bool = False, force_first: str | None = None
|
||||
) -> None:
|
||||
"""Generic powerup drop."""
|
||||
from bascenev1lib.actor import powerupbox
|
||||
|
||||
if standard_points:
|
||||
pts = self.map.powerup_spawn_points
|
||||
for i in range(len(pts)):
|
||||
bs.timer(
|
||||
1.0 + i * 0.5,
|
||||
bs.WeakCall(
|
||||
self._drop_powerup, i, force_first if i == 0 else None
|
||||
),
|
||||
)
|
||||
else:
|
||||
drop_pt = (
|
||||
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.PowerupBox(
|
||||
position=drop_pt,
|
||||
poweruptype=PowerupBoxFactory.get().get_random_powerup_type(
|
||||
excludetypes=self._excludepowerups
|
||||
),
|
||||
).autoretain()
|
||||
|
||||
def do_end(self, outcome: str) -> None:
|
||||
"""End the game."""
|
||||
if outcome == 'defeat':
|
||||
self.fade_to_red()
|
||||
self.end(
|
||||
delay=2.0,
|
||||
results={
|
||||
'outcome': outcome,
|
||||
'score': self._score,
|
||||
'playerinfos': self.initialplayerinfos,
|
||||
},
|
||||
)
|
||||
|
||||
def _update_bots(self) -> None:
|
||||
assert self._bot_update_interval is not None
|
||||
self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98)
|
||||
self._bot_update_timer = bs.Timer(
|
||||
self._bot_update_interval, bs.WeakCall(self._update_bots)
|
||||
)
|
||||
botspawnpts: list[Sequence[float]] = [
|
||||
[-5.0, 5.5, -4.14],
|
||||
[0.0, 5.5, -4.14],
|
||||
[5.0, 5.5, -4.14],
|
||||
]
|
||||
dists = [0.0, 0.0, 0.0]
|
||||
playerpts: list[Sequence[float]] = []
|
||||
for player in self.players:
|
||||
try:
|
||||
if player.is_alive():
|
||||
assert isinstance(player.actor, PlayerSpaz)
|
||||
assert player.actor.node
|
||||
playerpts.append(player.actor.node.position)
|
||||
except Exception:
|
||||
logging.exception('Error updating bots.')
|
||||
for i in range(3):
|
||||
for playerpt in playerpts:
|
||||
dists[i] += abs(playerpt[0] - botspawnpts[i][0])
|
||||
dists[i] += random.random() * 5.0 # Minor random variation.
|
||||
if dists[0] > dists[1] and dists[0] > dists[2]:
|
||||
spawnpt = botspawnpts[0]
|
||||
elif dists[1] > dists[2]:
|
||||
spawnpt = botspawnpts[1]
|
||||
else:
|
||||
spawnpt = botspawnpts[2]
|
||||
|
||||
spawnpt = (
|
||||
spawnpt[0] + 3.0 * (random.random() - 0.5),
|
||||
spawnpt[1],
|
||||
2.0 * (random.random() - 0.5) + spawnpt[2],
|
||||
)
|
||||
|
||||
# Normalize our bot type total and find a random number within that.
|
||||
total = 0.0
|
||||
for spawninfo in self._bot_spawn_types.values():
|
||||
total += spawninfo.spawnrate
|
||||
randval = random.random() * total
|
||||
|
||||
# Now go back through and see where this value falls.
|
||||
total = 0
|
||||
bottype: type[SpazBot] | None = None
|
||||
for spawntype, spawninfo in self._bot_spawn_types.items():
|
||||
total += spawninfo.spawnrate
|
||||
if randval <= total:
|
||||
bottype = spawntype
|
||||
break
|
||||
spawn_time = 1.0
|
||||
assert bottype is not None
|
||||
self._bots.spawn_bot(bottype, pos=spawnpt, spawn_time=spawn_time)
|
||||
|
||||
# After every spawn we adjust our ratios slightly to get more
|
||||
# difficult.
|
||||
for spawninfo in self._bot_spawn_types.values():
|
||||
spawninfo.spawnrate += spawninfo.increase
|
||||
spawninfo.increase += spawninfo.dincrease
|
||||
|
||||
def _update_scores(self) -> None:
|
||||
score = self._score
|
||||
|
||||
# Achievements apply to the default preset only.
|
||||
if self._preset == 'default':
|
||||
if score >= 250:
|
||||
self._award_achievement('Last Stand Master')
|
||||
if score >= 500:
|
||||
self._award_achievement('Last Stand Wizard')
|
||||
if score >= 1000:
|
||||
self._award_achievement('Last Stand God')
|
||||
assert self._scoreboard is not None
|
||||
self._scoreboard.set_team_value(self.teams[0], score, max_score=None)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, bs.PlayerDiedMessage):
|
||||
player = msg.getplayer(Player)
|
||||
self.stats.player_was_killed(player)
|
||||
bs.timer(0.1, self._checkroundover)
|
||||
|
||||
elif isinstance(msg, bs.PlayerScoredMessage):
|
||||
self._score += msg.score
|
||||
self._update_scores()
|
||||
|
||||
elif isinstance(msg, SpazBotDiedMessage):
|
||||
pts, importance = msg.spazbot.get_death_points(msg.how)
|
||||
target: Sequence[float] | None
|
||||
if msg.killerplayer:
|
||||
assert msg.spazbot.node
|
||||
target = msg.spazbot.node.position
|
||||
self.stats.player_scored(
|
||||
msg.killerplayer,
|
||||
pts,
|
||||
target=target,
|
||||
kill=True,
|
||||
screenmessage=False,
|
||||
importance=importance,
|
||||
)
|
||||
diesound = (
|
||||
self._dingsound if importance == 1 else self._dingsoundhigh
|
||||
)
|
||||
diesound.play(volume=0.6)
|
||||
|
||||
# Normally we pull scores from the score-set, but if there's no
|
||||
# player lets be explicit.
|
||||
else:
|
||||
self._score += pts
|
||||
self._update_scores()
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
|
||||
def end_game(self) -> None:
|
||||
# Tell our bots to celebrate just to rub it in.
|
||||
self._bots.final_celebrate()
|
||||
bs.setmusic(None)
|
||||
bs.pushcall(bs.WeakCall(self.do_end, 'defeat'))
|
||||
|
||||
def _checkroundover(self) -> None:
|
||||
"""End the round if conditions are met."""
|
||||
if not any(player.is_alive() for player in self.teams[0].players):
|
||||
self.end_game()
|
||||
Loading…
Add table
Add a link
Reference in a new issue