Bombsquad-Ballistica-Modded.../dist/ba_root/mods/games/lib.py
2022-02-14 12:22:10 +05:30

622 lines
No EOL
20 KiB
Python

# Released under the MIT License. See LICENSE for details.
#
"""Defines the King of the Hill game."""
# ba_meta require api 6
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import weakref
from enum import Enum
from typing import TYPE_CHECKING
import ba
from bastd.actor.flag import Flag
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.gameutils import SharedObjects
from dataclasses import dataclass
if TYPE_CHECKING:
from typing import Any, Optional, Sequence, Union
class FlagState(Enum):
"""States our single flag can be in."""
NEW = 0
UNCONTESTED = 1
CONTESTED = 2
HELD = 3
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
self.time_at_flag = 0
self.lives = 0
self.icons: list[Icon] = []
self.death_time: Optional[float] = None
self.distance_txt: Optional[ba.Node] = None
self.last_region = 0
self.lap = 0
self.distance = 0.0
self.finished = False
self.rank: Optional[int] = None
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self, time_remaining=None) -> None:
self.time_remaining = time_remaining
self.survival_seconds: Optional[int] = None
self.spawn_order: list[Player] = []
self.score = 0
self.time: Optional[float] = None
self.lap = 0
self.finished = False
#def __init__(self):
# self.survival_seconds: Optional[int] = None
# self.spawn_order: list[Player] = []
# self.score = 0
# self.time: Optional[float] = None
# self.lap = 0
# self.finished = False
class kth(ba.TeamGameActivity[Player, Team]):
"""Game where a team wins by holding a 'hill' for a set amount of time."""
name = 'King of the Hillx'
description = 'Secure the flag for a set length of time.'
available_settings = [
ba.IntSetting(
'Hold Time',
min_value=10,
default=30,
increment=10,
),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
]
scoreconfig = ba.ScoreConfig(label='Time Held')
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return issubclass(sessiontype, ba.MultiTeamSession)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('king_of_the_hill')
def __init__(self, settings: dict):
super().__init__(settings)
def get_instance_description(self) -> Union[str, Sequence]:
return 'Secure the flag for ${ARG1} seconds.', self._hold_time
def get_instance_description_short(self) -> Union[str, Sequence]:
return 'secure the flag for ${ARG1} seconds', self._hold_time
def on_begin(self) -> None:
super().on_begin()
shared = SharedObjects.get()
def end_game(self) -> None:
results = ba.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 handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
super().handlemessage(msg) # Augment default.
# =====================================================================================================================================================================
from bastd.actor.spazfactory import SpazFactory
if TYPE_CHECKING:
from typing import Any, Sequence, Optional, Union
class Icon(ba.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 = ba.gettexture('characterIconMask')
icon = player.get_icon()
self.node = ba.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 = ba.newnode(
'text',
owner=self.node,
attrs={
'text': ba.Lstr(value=player.getname()),
'color': ba.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 = ba.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:
ba.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:
ba.timer(0.6, self.update_for_lives)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.DieMessage):
self.node.delete()
return None
return super().handlemessage(msg)
class eli(ba.TeamGameActivity[Player, Team]):
"""Game type where last player(s) left alive win."""
name = 'Eliminationx'
description = 'Last remaining alive wins.'
scoreconfig = ba.ScoreConfig(label='Survived',
scoretype=ba.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[ba.Session]) -> list[ba.Setting]:
settings = [
ba.IntSetting(
'Lives Per Player',
default=1,
min_value=1,
max_value=10,
increment=1,
),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
if issubclass(sessiontype, ba.DualTeamSession):
settings.append(ba.BoolSetting('Solo Mode', default=False))
settings.append(
ba.BoolSetting('Balance Total Lives', default=False))
return settings
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return (issubclass(sessiontype, ba.DualTeamSession)
or issubclass(sessiontype, ba.FreeForAllSession))
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('melee')
def __init__(self, settings: dict):
super().__init__(settings)
def get_instance_description(self) -> Union[str, Sequence]:
return 'Last team standing wins.' if isinstance(
self.session, ba.DualTeamSession) else 'Last one standing wins.'
def get_instance_description_short(self) -> Union[str, Sequence]:
return 'last team standing wins' if isinstance(
self.session, ba.DualTeamSession) else 'last one standing wins'
def on_begin(self) -> None:
super().on_begin()
# ========================
# ba_meta export game
class dm(ba.TeamGameActivity[Player, Team]):
"""A game type based on acquiring kills."""
name = 'Death Matchx'
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[ba.Session]) -> list[ba.Setting]:
settings = [
ba.IntSetting(
'Kills to Win Per Player',
min_value=1,
default=5,
increment=1,
),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
# 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, ba.FreeForAllSession):
settings.append(
ba.BoolSetting('Allow Negative Scores', default=False))
return settings
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return (issubclass(sessiontype, ba.DualTeamSession)
or issubclass(sessiontype, ba.FreeForAllSession))
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('melee')
def __init__(self, settings: dict):
super().__init__(settings)
def get_instance_description(self) -> Union[str, Sequence]:
return 'Crush ${ARG1} of your enemies.', self._score_to_win
def get_instance_description_short(self) -> Union[str, Sequence]:
return 'kill ${ARG1} enemies', self._score_to_win
def on_begin(self) -> None:
super().on_begin()
# ====================
# ba_meta export game
class ms(ba.TeamGameActivity[Player, Team]):
"""Minigame involving dodging falling bombs."""
name = 'Meteor Showerx'
description = 'Dodge the falling bombs.'
available_settings = [ba.BoolSetting('Epic Mode', default=False)]
scoreconfig = ba.ScoreConfig(label='Survived',
scoretype=ba.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[ba.Session]) -> list[str]:
return ['Rampage']
# We support teams, free-for-all, and co-op sessions.
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return (issubclass(sessiontype, ba.DualTeamSession)
or issubclass(sessiontype, ba.FreeForAllSession)
or issubclass(sessiontype, ba.CoopSession))
def __init__(self, settings: dict):
super().__init__(settings)
def on_begin(self) -> None:
super().on_begin()
# =====================
@dataclass
class RaceMine:
"""Holds info about a mine on the track."""
point: Sequence[float]
mine: Optional[Bomb]
class RaceRegion(ba.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 = ba.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]
})
# ba_meta export game
class rg(ba.TeamGameActivity[Player, Team]):
"""Game of racing around a track."""
name = 'Racex'
description = 'Run real fast!'
scoreconfig = ba.ScoreConfig(label='Time',
lower_is_better=True,
scoretype=ba.ScoreType.MILLISECONDS)
@classmethod
def get_available_settings(
cls, sessiontype: type[ba.Session]) -> list[ba.Setting]:
settings = [
ba.IntSetting('Laps', min_value=1, default=3, increment=1),
ba.IntChoiceSetting(
'Time Limit',
default=0,
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
),
ba.IntChoiceSetting(
'Mine Spawning',
default=4000,
choices=[
('No Mines', 0),
('8 Seconds', 8000),
('4 Seconds', 4000),
('2 Seconds', 2000),
],
),
ba.IntChoiceSetting(
'Bomb Spawning',
choices=[
('None', 0),
('8 Seconds', 8000),
('4 Seconds', 4000),
('2 Seconds', 2000),
('1 Second', 1000),
],
default=2000,
),
ba.BoolSetting('Epic Mode', default=False),
]
# We have some specific settings in teams mode.
if issubclass(sessiontype, ba.DualTeamSession):
settings.append(
ba.BoolSetting('Entire Team Must Finish', default=False))
return settings
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return issubclass(sessiontype, ba.MultiTeamSession)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('race')
def __init__(self, settings: dict):
self._race_started = False
super().__init__(settings)
def get_instance_description(self) -> Union[str, Sequence]:
if (isinstance(self.session, ba.DualTeamSession)
and self._entire_team_must_finish):
t_str = ' Your entire team has to finish.'
else:
t_str = ''
if self._laps > 1:
return 'Run ${ARG1} laps.' + t_str, self._laps
return 'Run 1 lap.' + t_str
def get_instance_description_short(self) -> Union[str, Sequence]:
if self._laps > 1:
return 'run ${ARG1} laps', self._laps
return 'run 1 lap'
def on_player_leave(self, player: Player) -> None:
super().on_player_leave(player)
def on_begin(self) -> None:
from bastd.actor.onscreentimer import OnScreenTimer
super().on_begin()