hello API 8 !

This commit is contained in:
Ayush Saini 2023-08-13 17:21:49 +05:30
parent 3a2b6ade68
commit 0284fee95c
1166 changed files with 26061 additions and 375100 deletions

View file

@ -0,0 +1,2 @@
# Released under the MIT License. See LICENSE for details.
"""Our lovely collection of game related modules."""

View 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
)

View 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)

View 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
)

View 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

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

File diff suppressed because it is too large Load diff

View 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)

File diff suppressed because it is too large Load diff

View 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

View 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()