Added many minigames

This commit is contained in:
Loup-Garou911XD 2023-05-15 15:56:45 +05:30
parent 2e8ee6387e
commit 4af785998d
11 changed files with 5296 additions and 2 deletions

View file

@ -311,6 +311,151 @@
"md5sum": "4eb4a07830e4bd52fd19150a593d70c5"
}
}
},
"arms_race": {
"description": "Upgrade your weapons by eliminating enemies.Win by being first one to kill while cursed",
"external_url": "",
"authors": [
{
"name": "Freaku",
"email": "",
"discord": "[Just] Freak#4999"
}
],
"versions": {
"1.0.0": null
}
},
"collector": {
"description": "Kill your opponents to steal their Capsules.Collect them and score at the Deposist Point!",
"external_url": "",
"authors": [
{
"name": "TheMikirog",
"email": "",
"discord": "TheMikirog#1984"
},
{
"name": "JoseAng3l",
"email": "",
"discord": "! JoseANG3L#0268"
}
],
"versions": {
"1.0.0": null
}
},
"dodge_the_ball": {
"description": "Survive from shooting balls",
"external_url": "",
"authors": [
{
"name": "EmperoR",
"email": "",
"discord": "EmperoR#4098"
}
],
"versions": {
"1.0.0": null
}
},
"invisible_one": {
"description": "Be the invisible one for a length of time to win.Kill the invisible one to become it.",
"external_url": "",
"authors": [
{
"name": "itsre3",
"email": "",
"discord": "itsre3#0267"
}
],
"versions": {
"1.0.0": null
}
},
"last_punch_stand": {
"description": "",
"external_url": "Last one to punch the spaz when timer ends wins",
"authors": [
{
"name": "ThePersonMan",
"email": "",
"discord": "ThePersonMan#0276"
}
],
"versions": {
"1.0.0": null
}
},
"quake": {
"description": "Kill set number of enemies to win with optional rocket/railgun weapons",
"external_url": "",
"authors": [
{
"name": "Dliwk",
"email": "",
"discord": "Dliwk#7961"
}
],
"versions": {
"1.0.0": null
}
},
"sleep_race": {
"description": "Can you win while sleeping?",
"external_url": "",
"authors": [
{
"name": "itsre3",
"email": "",
"discord": "itsre3#0267"
}
],
"versions": {
"1.0.0": null
}
},
"snake": {
"description": "Survive a set number of mines to win",
"external_url": "",
"authors": [
{
"name": "Sebastian",
"email": "",
"discord": "SEBASTIAN2059#5751"
}
],
"versions": {
"1.0.0": null
}
},
"ufo_fight": {
"description": "Fight the UFO boss!",
"external_url": "",
"authors": [
{
"name": "Cross Joy",
"email": "cross.joy.official@gmail.com",
"discord": "Cross Joy#0721"
}
],
"versions": {
"1.0.0": null
}
},
"yeeting_party": {
"description": "Yeet your enemies out of the map!",
"external_url": "",
"authors": [
{
"name": "Freaku",
"email": "",
"discord": "[Just] Freak#4999"
}
],
"versions": {
"1.0.0": null
}
},
}
}

View file

@ -0,0 +1,194 @@
#Ported by: Freaku / @[Just] Freak#4999
#Join BCS:
# https://discord.gg/ucyaesh
# ba_meta require api 7
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.actor.playerspaz import PlayerSpaz
if TYPE_CHECKING:
from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional
class State:
def __init__(self, bomb=None, grab=False, punch=False, curse=False, required=False, final=False, name=''):
self.bomb = bomb
self.grab = grab
self.punch = punch
self.pickup = False
self.curse = curse
self.required = required or final
self.final = final
self.name = name
self.next = None
self.index = None
def apply(self, spaz):
spaz.disconnect_controls_from_player()
spaz.connect_controls_to_player(enable_punch=self.punch,
enable_bomb=self.bomb,
enable_pickup=self.grab)
if self.curse:
spaz.curse_time = -1
spaz.curse()
if self.bomb:
spaz.bomb_type = self.bomb
spaz.set_score_text(self.name)
def get_setting(self):
return (self.name)
states = [ State(bomb='normal', name='Basic Bombs'),
State(bomb='ice', name='Frozen Bombs'),
State(bomb='sticky', name='Sticky Bombs'),
State(bomb='impact', name='Impact Bombs'),
State(grab=True, name='Grabbing only'),
State(punch=True, name='Punching only'),
State(curse=True, name='Cursed', final=True) ]
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self):
self.state = None
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.score = 0
# ba_meta export game
class ArmsRaceGame(ba.TeamGameActivity[Player, Team]):
"""A game type based on acquiring kills."""
name = 'Arms Race'
description = 'Upgrade your weapon by eliminating enemies.\nWin the match by being the first player\nto get a kill while cursed.'
# 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.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)]
for state in states:
if not state.required:
settings.append(ba.BoolSetting(state.get_setting(), default=True))
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)
self.states = [s for s in states if settings.get(s.name, True)]
for i, state in enumerate(self.states):
if i < len(self.states) and not state.final:
state.next = self.states[i + 1]
state.index = i
self._dingsound = ba.getsound('dingSmall')
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 = (ba.MusicType.EPIC if self._epic_mode else
ba.MusicType.TO_THE_DEATH)
def get_instance_description(self) -> Union[str, Sequence]:
return 'Upgrade your weapon by eliminating enemies.'
def get_instance_description_short(self) -> Union[str, Sequence]:
return 'kill ${ARG1} enemies', len(self.states)
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
# self.setup_standard_powerup_drops()
def on_player_join(self, player):
if player.state is None:
player.state = self.states[0]
self.spawn_player(player)
# overriding the default character spawning..
def spawn_player(self, player):
if player.state is None:
player.state = self.states[0]
super().spawn_player(player)
player.state.apply(player.actor)
def isValidKill(self, m):
if m.getkillerplayer(Player) is None:
return False
if m.getkillerplayer(Player).team is m.getplayer(Player).team:
return False
return True
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
if self.isValidKill(msg):
self.stats.player_scored(msg.getkillerplayer(Player), 10, kill=True)
if not msg.getkillerplayer(Player).state.final:
msg.getkillerplayer(Player).state = msg.getkillerplayer(Player).state.next
msg.getkillerplayer(Player).state.apply(msg.getkillerplayer(Player).actor)
else:
msg.getkillerplayer(Player).team.score += 1
self.end_game()
self.respawn_player(msg.getplayer(Player))
else:
return super().handlemessage(msg)
return None
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.score)
self.end(results=results)

View file

@ -0,0 +1,642 @@
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
'''
Gamemode: Collector
Creator: TheMikirog
Website: https://bombsquadjoyride.blogspot.com/
This is a gamemode purely made by me just to spite unchallenged modders
out there that put out crap to the market.
We don't want gamemodes that are just the existing ones
with some novelties! Gamers deserve more!
In this gamemode you have to kill others in order to get their Capsules.
Capsules can be collected and staked in your inventory,
how many as you please.
After you kill an enemy that carries some of them,
they drop a respective amount of Capsules they carried + two more.
Your task is to collect these Capsules,
get to the flag and score them KOTH style.
You can't score if you don't have any Capsules with you.
The first player or team to get to the required ammount wins.
This is a gamemode all about trying to stay alive
and picking your battles in order to win.
A rare skill in BombSquad, where everyone is overly aggressive.
'''
from __future__ import annotations
import weakref
from enum import Enum
from typing import TYPE_CHECKING
import ba
import random
from bastd.actor.flag import Flag
from bastd.actor.popuptext import PopupText
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
lang = ba.app.lang.language
if lang == 'Spanish':
name = 'Coleccionista'
description = ('Elimina a tus oponentes para robar sus cápsulas.\n'
'¡Recolecta y anota en el punto de depósito!')
description_ingame = 'Obtén ${ARG1} cápsulas de tus enemigos.'
description_short = 'colecciona ${ARG1} cápsulas'
tips = [(
'¡Si tu oponente cae fuera del mapa, sus cápsulas desapareceran!\n'
'No intestes matar a tus enemigos arrojándolos al vacio.'),
'No te apresures. ¡Puedes perder tus cápsulas rápidamente!',
('¡No dejes que el jugador con más cápsulas anote!\n'
'¡Intenta atraparlo si puedes!'),
('¡Las Capsulas de la Suerte te dan 4 cápsulas en lugar de 2'
'y tienen un 8% de probabilidad de aparecer después de matar'),
('¡No te quedes en un solo lugar! Muevete más rapido que tu enemigo, '
'¡con suerte conseguirás algunas cápsulas!'),
]
capsules_to_win = 'Cápsulas para Ganar'
capsules_death = 'Cápsulas al Morir'
lucky_capsules = 'Cápsulas de la Suerte'
bonus = '¡BONUS!'
full_capacity = '¡Capacidad Completa!'
else:
name = 'Collector'
description = ('Kill your opponents to steal their Capsules.\n'
'Collect them and score at the Deposit point!')
description_ingame = 'Score ${ARG1} capsules from your enemies.'
description_short = 'collect ${ARG1} capsules'
tips = [(
'Making you opponent fall down the pit makes his Capsules wasted!\n'
'Try not to kill enemies by throwing them off the cliff.'),
'Don\'t be too reckless. You can lose your loot quite quickly!',
('Don\'t let the leading player score his Capsules '
'at the Deposit Point!\nTry to catch him if you can!'),
('Lucky Capsules give 4 to your inventory and they have 8% chance '
'of spawning after kill!'),
('Don\'t camp in one place! Make your move first, '
'so hopefully you get some dough!'),
]
capsules_to_win = 'Capsules to Win'
capsules_death = 'Capsules on Death'
lucky_capsules = 'Allow Lucky Capsules'
bonus = 'BONUS!'
full_capacity = 'Full Capacity!'
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.capsules = 0
self.light = None
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.score = 0
# ba_meta export game
class CollectorGame(ba.TeamGameActivity[Player, Team]):
name = name
description = description
tips = tips
# 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(
capsules_to_win,
min_value=1,
default=10,
increment=1,
),
ba.IntSetting(
capsules_death,
min_value=1,
max_value=10,
default=2,
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(lucky_capsules, default=True),
ba.BoolSetting('Epic Mode', 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('keep_away')
def __init__(self, settings: dict):
super().__init__(settings)
shared = SharedObjects.get()
self._scoreboard = Scoreboard()
self._score_to_win: int | None = None
self._swipsound = ba.getsound('swip')
self._lucky_sound = ba.getsound('ding')
self._flag_pos: Sequence[float] | None = None
self._flag_state: FlagState | None = None
self._flag: Flag | None = None
self._flag_light: ba.Node | None = None
self._scoring_team: weakref.ref[Team] | None = None
self._time_limit = float(settings['Time Limit'])
self._epic_mode = bool(settings['Epic Mode'])
self._capsules_to_win = int(settings[capsules_to_win])
self._capsules_death = int(settings[capsules_death])
self._lucky_capsules = bool(settings[lucky_capsules])
self._capsules: list[Any] = []
self._capsule_model = ba.getmodel('bomb')
self._capsule_tex = ba.gettexture('bombColor')
self._capsule_lucky_tex = ba.gettexture('bombStickyColor')
self._collect_sound = ba.getsound('powerup01')
self._lucky_collect_sound = ba.getsound('cashRegister2')
self._capsule_material = ba.Material()
self._capsule_material.add_actions(
conditions=('they_have_material', shared.player_material),
actions=('call', 'at_connect', self._on_capsule_player_collide),
)
self._flag_region_material = ba.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',
ba.Call(self._handle_player_flag_region_collide, True),
),
(
'call',
'at_disconnect',
ba.Call(self._handle_player_flag_region_collide, False),
),
),
)
# Base class overrides.
self.slow_motion = self._epic_mode
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SCARY
)
def get_instance_description(self) -> str | Sequence:
return description_ingame, self._score_to_win
def get_instance_description_short(self) -> str | Sequence:
return description_short, self._score_to_win
def create_team(self, sessionteam: ba.SessionTeam) -> Team:
return Team()
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
def on_begin(self) -> None:
super().on_begin()
shared = SharedObjects.get()
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._capsules_to_win * max(
1, max(len(t.players) for t in self.teams)
)
self._update_scoreboard()
if isinstance(self.session, ba.FreeForAllSession):
self._flag_pos = self.map.get_flag_position(random.randint(0, 1))
else:
self._flag_pos = self.map.get_flag_position(None)
ba.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 = ba.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]
ba.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()
if self._scoring_team is None:
scoring_team = None
else:
scoring_team = self._scoring_team()
if not scoring_team:
return
if isinstance(self.session, ba.FreeForAllSession):
players = self.players
else:
players = scoring_team.players
for player in players:
if player.time_at_flag > 0:
self.stats.player_scored(
player, 3, screenmessage=False, display=False
)
if player.capsules > 0:
if self._flag_state != FlagState.HELD:
return
if scoring_team.score >= self._score_to_win:
return
player.capsules -= 1
scoring_team.score += 1
self._handle_capsule_storage((
self._flag_pos[0],
self._flag_pos[1]+1,
self._flag_pos[2]
), player)
ba.playsound(
self._collect_sound,
0.8,
position=self._flag_pos)
self._update_scoreboard()
if player.capsules > 0:
assert self._flag is not None
self._flag.set_score_text(
str(self._score_to_win - scoring_team.score))
# winner
if scoring_team.score >= self._score_to_win:
self.end_game()
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.score)
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 = ba.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:
ba.playsound(self._swipsound)
def _handle_player_flag_region_collide(self, colliding: bool) -> None:
try:
spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
except ba.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.score, self._score_to_win
)
def _drop_capsule(self, player: Player) -> None:
pt = player.node.position
# Throw out capsules that the victim has + 2 more to keep the game running
for i in range(player.capsules + self._capsules_death):
# How far from each other these capsules should spawn
w = 0.6
# How much these capsules should fly after spawning
s = 0.005 - (player.capsules * 0.01)
self._capsules.append(
Capsule(
position=(pt[0] + random.uniform(-w, w),
pt[1] + 0.75 + random.uniform(-w, w),
pt[2]),
velocity=(random.uniform(-s, s),
random.uniform(-s, s),
random.uniform(-s, s)),
lucky=False))
if random.randint(1, 12) == 1 and self._lucky_capsules:
# How far from each other these capsules should spawn
w = 0.6
# How much these capsules should fly after spawning
s = 0.005
self._capsules.append(
Capsule(
position=(pt[0] + random.uniform(-w, w),
pt[1] + 0.75 + random.uniform(-w, w),
pt[2]),
velocity=(random.uniform(-s, s),
random.uniform(-s, s),
random.uniform(-s, s)),
lucky=True))
def _on_capsule_player_collide(self) -> None:
if self.has_ended():
return
collision = ba.getcollision()
# Be defensive here; we could be hitting the corpse of a player
# who just left/etc.
try:
capsule = collision.sourcenode.getdelegate(Capsule, True)
player = collision.opposingnode.getdelegate(
PlayerSpaz, True
).getplayer(Player, True)
except ba.NotFoundError:
return
if not player.is_alive():
return
if capsule.node.color_texture == self._capsule_lucky_tex:
player.capsules += 4
PopupText(
bonus,
color=(1, 1, 0),
scale=1.5,
position=capsule.node.position
).autoretain()
ba.playsound(
self._lucky_collect_sound,
1.0,
position=capsule.node.position)
ba.emitfx(
position=capsule.node.position,
velocity=(0, 0, 0),
count=int(6.4+random.random()*24),
scale=1.2,
spread=2.0,
chunk_type='spark');
ba.emitfx(
position=capsule.node.position,
velocity=(0, 0, 0),
count=int(4.0+random.random()*6),
emit_type='tendrils');
else:
player.capsules += 1
ba.playsound(
self._collect_sound,
0.6,
position=capsule.node.position)
# create a flash
light = ba.newnode(
'light',
attrs={
'position': capsule.node.position,
'height_attenuated': False,
'radius': 0.1,
'color': (1, 1, 0)})
# Create a short text informing about your inventory
self._handle_capsule_storage(player.position, player)
ba.animate(light, 'intensity', {
0: 0,
0.1: 0.5,
0.2: 0
}, loop=False)
ba.timer(0.2, light.delete)
capsule.handlemessage(ba.DieMessage())
def _update_player_light(self, player: Player, capsules: int) -> None:
if player.light:
intensity = 0.04 * capsules
ba.animate(player.light, 'intensity', {
0.0: player.light.intensity,
0.1: intensity
})
def newintensity():
player.light.intensity = intensity
ba.timer(0.1, newintensity)
else:
player.light = ba.newnode(
'light',
attrs={
'height_attenuated': False,
'radius': 0.2,
'intensity': 0.0,
'color': (0.2, 1, 0.2)
})
player.node.connectattr('position', player.light, 'position')
def _handle_capsule_storage(self, pos: float, player: Player) -> None:
capsules = player.capsules
text = str(capsules)
scale = 1.75 + (0.02 * capsules)
if capsules > 10:
player.capsules = 10
text = full_capacity
color = (1, 0.85, 0)
elif capsules > 7:
color = (1, 0, 0)
scale = 2.4
elif capsules > 5:
color = (1, 0.4, 0.4)
scale = 2.1
elif capsules > 3:
color = (1, 1, 0.4)
scale = 2.0
else:
color = (1, 1, 1)
scale = 1.9
PopupText(
text,
color=color,
scale=scale,
position=(pos[0], pos[1]-1, pos[2])
).autoretain()
self._update_player_light(player, capsules)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.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._drop_capsule(player)
player.capsules = 0
self._update_player_light(player, 0)
self.respawn_player(player)
else:
return super().handlemessage(msg)
class Capsule(ba.Actor):
def __init__(self,
position: Sequence[float] = (0.0, 1.0, 0.0),
velocity: Sequence[float] = (0.0, 0.5, 0.0),
lucky: bool = False):
super().__init__()
shared = SharedObjects.get()
activity = self.getactivity()
# spawn just above the provided point
self._spawn_pos = (position[0], position[1], position[2])
if lucky:
ba.playsound(activity._lucky_sound, 1.0, self._spawn_pos)
self.node = ba.newnode(
'prop',
attrs={
'model': activity._capsule_model,
'color_texture': activity._capsule_lucky_tex if lucky else (
activity._capsule_tex),
'body': 'crate' if lucky else 'capsule',
'reflection': 'powerup' if lucky else 'soft',
'body_scale': 0.65 if lucky else 0.3,
'density':6.0 if lucky else 4.0,
'reflection_scale': [0.15],
'shadow_size': 0.65 if lucky else 0.6,
'position': self._spawn_pos,
'velocity': velocity,
'materials': [
shared.object_material, activity._capsule_material]
},
delegate=self)
ba.animate(self.node, 'model_scale', {
0.0: 0.0,
0.1: 0.9 if lucky else 0.6,
0.16: 0.8 if lucky else 0.5
})
self._light_capsule = ba.newnode(
'light',
attrs={
'position': self._spawn_pos,
'height_attenuated': False,
'radius': 0.5 if lucky else 0.1,
'color': (0.2, 0.2, 0) if lucky else (0.2, 1, 0.2)
})
self.node.connectattr('position', self._light_capsule, 'position')
def handlemessage(self, msg: Any):
if isinstance(msg, ba.DieMessage):
self.node.delete()
ba.animate(self._light_capsule, 'intensity', {
0: 1.0,
0.05: 0.0
}, loop=False)
ba.timer(0.05, self._light_capsule.delete)
elif isinstance(msg, ba.OutOfBoundsMessage):
self.handlemessage(ba.DieMessage())
elif isinstance(msg, ba.HitMessage):
self.node.handlemessage(
'impulse',
msg.pos[0], msg.pos[1], msg.pos[2],
msg.velocity[0]/8, msg.velocity[1]/8, msg.velocity[2]/8,
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:
return super().handlemessage(msg)

View file

@ -0,0 +1,762 @@
"""
DondgeTheBall minigame by EmperoR#4098
"""
# Feel free to edit.
# ba_meta require api 7
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from random import choice
from enum import Enum
from bastd.actor.bomb import Blast
from bastd.actor.popuptext import PopupText
from bastd.actor.powerupbox import PowerupBox
from bastd.actor.onscreencountdown import OnScreenCountdown
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import NoReturn, Sequence, Any
# Type of ball in this game
class BallType(Enum):
""" Types of ball """
EASY = 0
# Decrease the next ball shooting speed(not ball speed).
# Ball color is yellow.
MEDIUM = 1
# increase the next ball shooting speed(not ball speed).
# target the head of player.
# Ball color is purple.
HARD = 2
# Target player according to player movement (not very accurate).
# Taget: player head.
# increase the next ball speed but less than MEDIUM.
# Ball color is crimson(purple+red = pinky color type).
# this dict decide the ball_type spawning rate like powerup box
ball_type_dict: dict[BallType, int] = {
BallType.EASY: 3,
BallType.MEDIUM: 2,
BallType.HARD: 1,
};
class Ball(ba.Actor):
""" Shooting Ball """
def __init__(self,
position: Sequence[float],
velocity: Sequence[float],
texture: ba.Texture,
body_scale: float = 1.0,
gravity_scale: float = 1.0,
) -> NoReturn:
super().__init__();
shared = SharedObjects.get();
ball_material = ba.Material();
ball_material.add_actions(
conditions=(
(
('we_are_younger_than', 100),
'or',
('they_are_younger_than', 100),
),
'and',
('they_have_material', shared.object_material),
),
actions=('modify_node_collision', 'collide', False),
);
self.node = ba.newnode(
'prop',
delegate=self,
attrs={
'body': 'sphere',
'position': position,
'velocity': velocity,
'body_scale': body_scale,
'model': ba.getmodel('frostyPelvis'),
'model_scale': body_scale,
'color_texture': texture,
'gravity_scale': gravity_scale,
'density': 4.0, # increase density of ball so ball collide with player with heavy force. # ammm very bad grammer
'materials': (ball_material,),
},
);
# die the ball manually incase the ball doesn't fall the outside of the map
ba.timer(2.5, ba.WeakCall(self.handlemessage, ba.DieMessage()));
# i am not handling anything in this ball Class(except for diemessage).
# all game things and logics going to be in the box class
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.DieMessage):
self.node.delete();
else:
super().handlemessage(msg);
class Box(ba.Actor):
""" A box that spawn midle of map as a decoration perpose """
def __init__(self,
position: Sequence[float],
velocity: Sequence[float],
) -> NoReturn:
super().__init__();
shared = SharedObjects.get();
# self.ball_jump = 0.0;
no_hit_material = ba.Material();
# we don't need that the box was move and collide with objects.
no_hit_material.add_actions(
conditions=(
('they_have_material', shared.pickup_material),
'or',
('they_have_material', shared.attack_material),
),
actions=('modify_part_collision', 'collide', False),
);
no_hit_material.add_actions(
conditions=(
('they_have_material', shared.object_material),
'or',
('they_dont_have_material', shared.footing_material),
),
actions=(
('modify_part_collision', 'collide', False),
('modify_part_collision', 'physical', False),
),
);
self.node = ba.newnode(
'prop',
delegate=self,
attrs={
'body': 'box',
'position': position,
'model': ba.getmodel('powerup'),
'light_model': ba.getmodel('powerupSimple'),
'shadow_size': 0.5,
'body_scale': 1.4,
'model_scale': 1.4,
'color_texture': ba.gettexture('landMineLit'),
'reflection': 'powerup',
'reflection_scale': [1.0],
'materials': (no_hit_material,),
},
);
# light
self.light = ba.newnode(
"light",
owner = self.node,
attrs={
'radius' : 0.2,
'intensity' : 0.8,
'color': (0.0, 1.0, 0.0),
}
);
self.node.connectattr("position", self.light, "position");
# Drawing circle and circleOutline in radius of 3,
# so player can see that how close he is to the box.
# If player is inside this circle the ball speed will increase.
circle = ba.newnode(
"locator",
owner = self.node,
attrs = {
'shape': 'circle',
'color': (1.0, 0.0, 0.0),
'opacity': 0.1,
'size': (6.0, 0.0, 6.0),
'draw_beauty': False,
'additive': True,
},
)
self.node.connectattr("position", circle, "position");
# also adding a outline cause its look nice.
circle_outline = ba.newnode(
"locator",
owner = self.node,
attrs = {
'shape': 'circleOutline',
'color': (1.0, 1.0, 0.0),
'opacity': 0.1,
'size': (6.0, 0.0, 6.0),
'draw_beauty': False,
'additive': True,
},
);
self.node.connectattr("position", circle_outline, "position");
# all ball attribute that we need.
self.ball_type: BallType = BallType.EASY;
self.shoot_timer: ba.Timer | None = None;
self.shoot_speed: float = 0.0;
# this force the shoot if player is inside the red circle.
self.force_shoot_speed: float = 0.0;
self.ball_mag = 3000;
self.ball_gravity: float = 1.0;
self.ball_tex: ba.Texture | None = None;
# only for Hard ball_type
self.player_facing_direction: list[float, float] = [0.0, 0.0];
# ball shoot soound.
self.shoot_sound = ba.getsound('laserReverse');
# same as "powerupdist"
self.ball_type_dist: list[BallType] = [];
for ball in ball_type_dict:
for _ in range(ball_type_dict[ball]):
self.ball_type_dist.append(ball);
# Here main logic of game goes here.
# like shoot balls, shoot speed, anything we want goes here(except for some thing).
def start_shoot(self) -> NoReturn:
# getting all allive players in a list.
alive_players_list = self.activity.get_alive_players();
# make sure that list is not Empty.
if len(alive_players_list) > 0:
# choosing a random player from list.
target_player = choice(alive_players_list);
# highlight the target player
self.highlight_target_player(target_player);
# to finding difference between player and box.
# we just need to subtract player pos and ball pos.
# Same logic as eric applied in Target Practice Gamemode.
difference = ba.Vec3(target_player.position) - ba.Vec3(self.node.position);
# discard Y position so ball shoot more straight.
difference[1] = 0.0
# and now, this length method returns distance in float.
# we're gonna use this value for calculating player analog stick
distance = difference.length();
# shoot a random BallType
self.upgrade_ball_type(choice(self.ball_type_dist));
# and check the ball_type and upgrade it gravity_scale, texture, next ball speed.
self.check_ball_type(self.ball_type);
# For HARD ball i am just focusing on player analog stick facing direction.
# Not very accurate and that's we need.
if self.ball_type == BallType.HARD:
self.calculate_player_analog_stick(target_player, distance);
else:
self.player_facing_direction = [0.0, 0.0];
pos = self.node.position;
if self.ball_type == BallType.MEDIUM or self.ball_type == BallType.HARD:
# Target head by increasing Y pos.
# How this work? cause ball gravity_scale is ......
pos = (pos[0], pos[1]+.25, pos[2]);
# ball is generating..
ball = Ball(
position = pos,
velocity = (0.0, 0.0, 0.0),
texture = self.ball_tex,
gravity_scale = self.ball_gravity,
body_scale = 1.0,
).autoretain();
# shoot Animation and sound.
self.shoot_animation();
# force the shoot speed if player try to go inside the red circle.
if self.force_shoot_speed != 0.0:
self.shoot_speed = self.force_shoot_speed;
# push the ball to the player
ball.node.handlemessage(
'impulse',
self.node.position[0], # ball spawn position X
self.node.position[1], # Y
self.node.position[2], # Z
0, 0, 0, # velocity x,y,z
self.ball_mag, # magnetude
0.000, # magnetude velocity
0.000, # radius
0.000, # idk
difference[0] + self.player_facing_direction[0], # force direction X
difference[1] , # force direction Y
difference[2] + self.player_facing_direction[1], # force direction Z
);
# creating our timer and shoot the ball again.(and we create a loop)
self.shoot_timer = ba.Timer(self.shoot_speed, self.start_shoot);
def upgrade_ball_type(self, ball_type: BallType) -> NoReturn:
self.ball_type = ball_type;
def check_ball_type(self, ball_type: BallType) -> NoReturn:
if ball_type == BallType.EASY:
self.shoot_speed = 0.8;
self.ball_gravity = 1.0;
# next ball shoot speed
self.ball_mag = 3000;
# box light color and ball tex
self.light.color = (1.0, 1.0, 0.0);
self.ball_tex = ba.gettexture('egg4');
elif ball_type == BallType.MEDIUM:
self.ball_mag = 3000;
# decrease the gravity scale so, ball shoot without falling and straight.
self.ball_gravity = 0.0;
# next ball shoot speed.
self.shoot_speed = 0.4;
# box light color and ball tex.
self.light.color = (1.0, 0.0, 1.0);
self.ball_tex = ba.gettexture('egg3');
elif ball_type == BallType.HARD:
self.ball_mag = 2500;
self.ball_gravity = 0.0;
# next ball shoot speed.
self.shoot_speed = 0.6;
# box light color and ball tex.
self.light.color = (1.0, 0.2, 1.0);
self.ball_tex = ba.gettexture('egg1');
def shoot_animation(self) -> NoReturn:
ba.animate(
self.node,
"model_scale",{
0.00: 1.4,
0.05: 1.7,
0.10: 1.4,
}
);
# playing shoot sound.
ba.playsound(self.shoot_sound, position = self.node.position);
def highlight_target_player(self, player: ba.Player) -> NoReturn:
# adding light
light = ba.newnode(
"light",
owner = self.node,
attrs={
'radius':0.0,
'intensity':1.0,
'color': (1.0, 0.0, 0.0),
}
);
ba.animate(
light,
"radius",{
0.05: 0.02,
0.10: 0.07,
0.15: 0.15,
0.20: 0.13,
0.25: 0.10,
0.30: 0.05,
0.35: 0.02,
0.40: 0.00,
}
);
# And a circle outline with ugly animation.
circle_outline = ba.newnode(
"locator",
owner = player.actor.node,
attrs={
'shape': 'circleOutline',
'color': (1.0, 0.0, 0.0),
'opacity': 1.0,
'draw_beauty': False,
'additive': True,
},
);
ba.animate_array(
circle_outline,
'size',
1, {
0.05: [0.5],
0.10: [0.8],
0.15: [1.5],
0.20: [2.0],
0.25: [1.8],
0.30: [1.3],
0.35: [0.6],
0.40: [0.0],
}
);
# coonect it and...
player.actor.node.connectattr("position", light, "position");
player.actor.node.connectattr("position", circle_outline, "position");
# immediately delete the node after another player has been targeted.
self.shoot_speed = 0.5 if self.shoot_speed == 0.0 else self.shoot_speed;
ba.timer(self.shoot_speed, light.delete);
ba.timer(self.shoot_speed, circle_outline.delete);
def calculate_player_analog_stick(self, player:ba.Player, distance: float) -> NoReturn:
# at first i was very confused how i can read the player analog stick \
# then i saw TheMikirog#1984 autorun plugin code.
# and i got it how analog stick values are works.
# just need to store analog stick facing direction and need some calculation according how far player pushed analog stick.
# Notice that how vertical direction is inverted, so we need to put a minus infront of veriable.(so ball isn't shoot at wrong direction).
self.player_facing_direction[0] = player.actor.node.move_left_right;
self.player_facing_direction[1] = -player.actor.node.move_up_down;
# if player is too close and the player pushing his analog stick fully the ball shoot's too far away to player.
# so, we need to reduce the value of "self.player_facing_direction" to fix this problem.
if distance <= 3:
self.player_facing_direction[0] = 0.4 if self.player_facing_direction[0] > 0 else -0.4;
self.player_facing_direction[1] = 0.4 if self.player_facing_direction[0] > 0 else -0.4;
# same problem to long distance but in reverse, the ball can't reach to the player,
# its because player analog stick value is between 1 and -1,
# and this value is low to shoot ball forward to Player if player is too far from the box.
# so. let's increase to 1.5 if player pushed analog stick fully.
elif distance > 6.5:
# So many calculation according to how analog stick pushed by player.
# Horizontal(left-right) calculation
if self.player_facing_direction[0] > 0.4:
self.player_facing_direction[0] = 1.5;
elif self.player_facing_direction[0] < -0.4:
self.player_facing_direction[0] = -1.5;
else:
if self.player_facing_direction[0] > 0.0:
self.player_facing_direction[0] = 0.2;
elif self.player_facing_direction[0] < 0.0:
self.player_facing_direction[0] = -0.2;
else:
self.player_facing_direction[0] = 0.0;
# Vertical(up-down) calculation.
if self.player_facing_direction[1] > 0.4:
self.player_facing_direction[1] = 1.5;
elif self.player_facing_direction[1] < -0.4:
self.player_facing_direction[1] = -1.5;
else:
if self.player_facing_direction[1] > 0.0:
self.player_facing_direction[1] = 0.2;
elif self.player_facing_direction[1] < 0.0:
self.player_facing_direction[1] = -0.2;
else:
self.player_facing_direction[1] = -0.0;
# if we want stop the ball shootes
def stop_shoot(self) -> NoReturn:
# Kill the timer.
self.shoot_timer = None;
class Player(ba.Player['Team']):
"""Our player type for this game."""
class Team(ba.Team[Player]):
"""Our team type for this game."""
# almost 80 % for game we done in box class.
# now remain things, like name, seetings, scoring, cooldonw,
# and main thing don't allow player to camp inside of box are going in this class.
# ba_meta export game
class DodgeTheBall(ba.TeamGameActivity[Player, Team]):
# defining name, description and settings..
name = 'Dodge the ball';
description = 'Survive from shooting balls';
available_settings = [
ba.IntSetting(
'Cooldown',
min_value = 20,
default = 45,
increment = 5,
),
ba.BoolSetting('Epic Mode', default=False)
]
# Don't allow joining after we start.
allow_mid_activity_joins = False;
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
# We support team and ffa sessions.
return issubclass(sessiontype, ba.FreeForAllSession) or issubclass(
sessiontype, ba.DualTeamSession,
);
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
# This Game mode need a flat and perfect shape map where can player fall outside map.
# bombsquad have "Doom Shroom" map.
# Not perfect map for this game mode but its fine for this gamemode.
# the problem is that Doom Shroom is not a perfect circle and not flat also.
return ['Doom Shroom'];
def __init__(self, settings: dict):
super().__init__(settings);
self._epic_mode = bool(settings['Epic Mode']);
self.countdown_time = int(settings['Cooldown']);
self.check_player_pos_timer: ba.Timer | None = None;
self.shield_drop_timer: ba.Timer | None = None;
# cooldown and Box
self._countdown: OnScreenCountdown | None = None;
self.box: Box | None = None;
# this lists for scoring.
self.joined_player_list: list[ba.Player] = [];
self.dead_player_list: list[ba.Player] = [];
# normally play RUN AWAY music cause is match with our gamemode at.. my point,
# but in epic switch to EPIC.
self.slow_motion = self._epic_mode;
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.RUN_AWAY
);
def get_instance_description(self) -> str | Sequence:
return 'Keep away as possible as you can';
# add a tiny text under our game name.
def get_instance_description_short(self) -> str | Sequence:
return 'Dodge the shooting balls';
def on_begin(self) -> NoReturn:
super().on_begin();
# spawn our box at middle of the map
self.box = Box(
position=(0.5, 2.7, -3.9),
velocity=(0.0, 0.0, 0.0),
).autoretain();
# create our cooldown
self._countdown = OnScreenCountdown(
duration = self.countdown_time,
endcall = self.play_victory_sound_and_end,
);
# and starts the cooldown and shootes.
ba.timer(5.0, self._countdown.start);
ba.timer(5.0, self.box.start_shoot);
# start checking all player pos.
ba.timer(5.0, self.check_player_pos);
# drop shield every ten Seconds
# need five seconds delay Because shootes start after 5 seconds.
ba.timer(15.0, self.drop_shield);
# This function returns all alive players in game.
# i thinck you see this function in Box class.
def get_alive_players(self) -> Sequence[ba.Player]:
alive_players = [];
for team in self.teams:
for player in team.players:
if player.is_alive():
alive_players.append(player);
return alive_players;
# let's disallowed camping inside of box by doing a blast and increasing ball shoot speed.
def check_player_pos(self):
for player in self.get_alive_players():
# same logic as applied for the ball
difference = ba.Vec3(player.position) - ba.Vec3(self.box.node.position);
distance = difference.length();
if distance < 3:
self.box.force_shoot_speed = 0.2;
else:
self.box.force_shoot_speed = 0.0;
if distance < 0.5:
Blast(
position = self.box.node.position,
velocity = self.box.node.velocity,
blast_type = 'normal',
blast_radius = 1.0,
).autoretain();
PopupText(
position = self.box.node.position,
text = 'Keep away from me',
random_offset = 0.0,
scale = 2.0,
color = self.box.light.color,
).autoretain();
# create our timer and start looping it
self.check_player_pos_timer = ba.Timer(0.1, self.check_player_pos);
# drop useless shield's too give player temptation.
def drop_shield(self) -> NoReturn:
pos = self.box.node.position;
PowerupBox(
position = (pos[0] + 4.0, pos[1] + 3.0, pos[2]),
poweruptype = 'shield',
).autoretain();
PowerupBox(
position = (pos[0] - 4.0, pos[1] + 3.0, pos[2]),
poweruptype = 'shield',
).autoretain();
self.shield_drop_timer = ba.Timer(10.0, self.drop_shield);
# when cooldown time up i don't want that the game end immediately.
def play_victory_sound_and_end(self) -> NoReturn:
# kill timers
self.box.stop_shoot();
self.check_player_pos_timer = None
self.shield_drop_timer = None
ba.timer(2.0, self.end_game);
# this function runs when A player spawn in map
def spawn_player(self, player: Player) -> NoReturn:
spaz = self.spawn_player_spaz(player);
# reconnect this player's controls.
# without bomb, punch and pickup.
spaz.connect_controls_to_player(
enable_punch=False, enable_bomb=False, enable_pickup=False,
);
# storing all players for ScorinG.
self.joined_player_list.append(player);
# Also lets have them make some noise when they die.
spaz.play_big_death_sound = True;
# very helpful function to check end game when player dead or leav.
def _check_end_game(self) -> bool:
living_team_count = 0
for team in self.teams:
for player in team.players:
if player.is_alive():
living_team_count += 1
break
if living_team_count <= 0:
# kill the coutdown timer incase the all players dead before game is about to going to be end.
# so, countdown won't call the function.
# FIXE ME: it's that ok to kill this timer?
# self._countdown._timer = None;
self.end_game()
# this function called when player leave.
def on_player_leave(self, player: Player) -> NoReturn:
# Augment default behavior.
super().on_player_leave(player);
# checking end game.
self._check_end_game();
# this gamemode needs to handle only one msg "PlayerDiedMessage".
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg);
# and storing the dead player records in our dead_player_list.
self.dead_player_list.append(msg.getplayer(Player));
# check the end game.
ba.timer(1.0, self._check_end_game);
def end_game(self):
# kill timers
self.box.stop_shoot()
self.check_player_pos_timer = None
self.shield_drop_timer = None
# here the player_dead_list and joined_player_list gonna be very helpful.
for team in self.teams:
for player in team.players:
# for scoring i am just following the index of the player_dead_list.
# for dead list...
# 0th index player dead first.
# 1st index player dead second.
# and so on...
# i think you got it... maybe
# sometime we also got a empty list
# if we got a empty list that means all players are survived or maybe only one player playing and he/she survived.
if len(self.dead_player_list) > 0:
for index, dead_player in enumerate(self.dead_player_list):
# if this condition is true we find the dead player \
# and his index with enumerate function.
if player == dead_player:
# updating with one, because i don't want to give 0 score to first dead player.
index += 1
break
# and if this statement is true we just find a survived player.
# for survived player i am giving the highest score according to how many players are joined.
elif index == len(self.dead_player_list) - 1:
index = len(self.joined_player_list)
# for survived player i am giving the highest score according to how many players are joined.
else:
index = len(self.joined_player_list)
# and here i am following Table of 10 for scoring.
# very lazY.
score = int(10 * index)
self.stats.player_scored(player, score, screenmessage=False)
# Ok now calc game results: set a score for each team and then tell \
# the game to end.
results = ba.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.
# hmmm... some eric comments might be helpful to you.
for team in self.teams:
max_index = 0
for player in team.players:
# for the team, we choose only one player who survived longest.
# same logic..
if len(self.dead_player_list) > 0:
for index, dead_player in enumerate(self.dead_player_list):
if player == dead_player:
index += 1
break
elif index == len(self.dead_player_list) - 1:
index = len(self.joined_player_list)
else:
index = len(self.joined_player_list)
max_index = max(max_index, index)
# set the team score
results.set_team_score(team, int(10 * max_index))
# and end the game
self.end(results=results)

View file

@ -0,0 +1,333 @@
# Released under the MIT License. See LICENSE for details.
#
# By itsre3
# =>3<=
# Don't mind my spelling. i realized that they were not correct after making last change and saving
# Besides that, enjoy.......!!
"""Provides the chosen-one mini-game."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
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
if TYPE_CHECKING:
from typing import Any, Type, List, Dict, Optional, Sequence, Union
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
self.chosen_light: Optional[ba.NodeActor] = None
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self, time_remaining: int) -> None:
self.time_remaining = time_remaining
# ba_meta export game
class InvicibleOneGame(ba.TeamGameActivity[Player, Team]):
"""
Game involving trying to remain the one 'invisible one'
for a set length of time while everyone else tries to
kill you and become the invisible one themselves.
"""
name = 'Invisible One'
description = ('Be the invisible one for a length of time to win.\n'
'Kill the invisible one to become it.')
available_settings = [
ba.IntSetting(
'Invicible One Time',
min_value=10,
default=30,
increment=10,
),
ba.BoolSetting('Invicible one is lazy', default=True),
ba.BoolSetting('Night mode', default=False),
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),
]
scoreconfig = ba.ScoreConfig(label='Time Held')
@classmethod
def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]:
return ba.getmaps('keep_away')
def __init__(self, settings: dict):
super().__init__(settings)
self._scoreboard = Scoreboard()
self._invicible_one_player: Optional[Player] = None
self._swipsound = ba.getsound('swip')
self._countdownsounds: Dict[int, ba.Sound] = {
10: ba.getsound('announceTen'),
9: ba.getsound('announceNine'),
8: ba.getsound('announceEight'),
7: ba.getsound('announceSeven'),
6: ba.getsound('announceSix'),
5: ba.getsound('announceFive'),
4: ba.getsound('announceFour'),
3: ba.getsound('announceThree'),
2: ba.getsound('announceTwo'),
1: ba.getsound('announceOne')
}
self._flag_spawn_pos: Optional[Sequence[float]] = None
self._reset_region_material: Optional[ba.Material] = None
self._flag: Optional[Flag] = None
self._reset_region: Optional[ba.Node] = None
self._epic_mode = bool(settings['Epic Mode'])
self._invicible_one_time = int(settings['Invicible One Time'])
self._time_limit = float(settings['Time Limit'])
self._invicible_one_is_lazy = bool(settings['Invicible one is lazy'])
self._night_mode = bool(settings['Night mode'])
# Base class overrides
self.slow_motion = self._epic_mode
self.default_music = (ba.MusicType.EPIC
if self._epic_mode else ba.MusicType.CHOSEN_ONE)
def get_instance_description(self) -> Union[str, Sequence]:
return 'Show your invisibility powers.'
def create_team(self, sessionteam: ba.SessionTeam) -> Team:
return Team(time_remaining=self._invicible_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_invicible_one_player() is player:
self._set_invicible_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)
self._set_invicible_one_player(None)
if self._night_mode:
gnode = ba.getactivity().globalsnode
gnode.tint = (0.4, 0.4, 0.4)
pos = self._flag_spawn_pos
ba.timer(1.0, call=self._tick, repeat=True)
mat = self._reset_region_material = ba.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',
ba.WeakCall(self._handle_reset_collide)),
),
)
self._reset_region = ba.newnode('region',
attrs={
'position': (pos[0], pos[1] + 0.75,
pos[2]),
'scale': (0.5, 0.5, 0.5),
'type': 'sphere',
'materials': [mat]
})
def _get_invicible_one_player(self) -> Optional[Player]:
# Should never return invalid references; return None in that case.
if self._invicible_one_player:
return self._invicible_one_player
return None
def _handle_reset_collide(self) -> None:
# If we have a chosen one, ignore these.
if self._get_invicible_one_player() is not None:
return
# Attempt to get a Player controlling a Spaz that we hit.
try:
player = ba.getcollision().opposingnode.getdelegate(
PlayerSpaz, True).getplayer(Player, True)
except ba.NotFoundError:
return
if player.is_alive():
self._set_invicible_one_player(player)
def _flash_flag_spawn(self) -> None:
light = ba.newnode('light',
attrs={
'position': self._flag_spawn_pos,
'color': (1, 1, 1),
'radius': 0.3,
'height_attenuated': False
})
ba.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True)
ba.timer(1.0, light.delete)
def _tick(self) -> None:
# Give the chosen one points.
player = self._get_invicible_one_player()
if player is not None:
# This shouldn't happen, but just in case.
if not player.is_alive():
ba.print_error('got dead player as chosen one in _tick')
self._set_invicible_one_player(None)
else:
scoring_team = player.team
assert self.stats
self.stats.player_scored(player,
3,
screenmessage=False,
display=False)
scoring_team.time_remaining = max(
0, scoring_team.time_remaining - 1)
self._update_scoreboard()
# announce numbers we have sounds for
if scoring_team.time_remaining in self._countdownsounds:
ba.playsound(
self._countdownsounds[scoring_team.time_remaining])
# 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._invicible_one_player is not None:
ba.print_error('got nonexistent player as chosen one in _tick')
self._set_invicible_one_player(None)
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
results.set_team_score(team,
self._invicible_one_time - team.time_remaining)
self.end(results=results, announce_delay=0)
def _set_invicible_one_player(self, player: Optional[Player]) -> None:
existing = self._get_invicible_one_player()
if existing:
existing.chosen_light = None
ba.playsound(self._swipsound)
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._invicible_one_player = None
# Create a light to highlight the flag;
# this will go away when the flag dies.
ba.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()
else:
if player.actor:
self._flag = None
self._invicible_one_player = player
if self._invicible_one_is_lazy:
player.actor.connect_controls_to_player(enable_punch = False, enable_pickup = False, enable_bomb = False)
if player.actor.node.torso_model != None:
player.actor.node.color_mask_texture = None
player.actor.node.color_texture = None
player.actor.node.head_model = None
player.actor.node.torso_model = None
player.actor.node.upper_arm_model = None
player.actor.node.forearm_model = None
player.actor.node.pelvis_model = None
player.actor.node.toes_model = None
player.actor.node.upper_leg_model = None
player.actor.node.lower_leg_model = None
player.actor.node.hand_model = None
player.actor.node.style = 'cyborg'
invi_sound = []
player.actor.node.jump_sounds = invi_sound
player.actor.attack_sounds = invi_sound
player.actor.impact_sounds = invi_sound
player.actor.pickup_sounds = invi_sound
player.actor.death_sounds = invi_sound
player.actor.fall_sounds = invi_sound
player.actor.node.name = ''
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
player = msg.getplayer(Player)
if player is self._get_invicible_one_player():
killerplayer = msg.getkillerplayer(Player)
self._set_invicible_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._invicible_one_time,
countdown=True)

View file

@ -0,0 +1,263 @@
# ba_meta require api 7
from typing import Sequence
import ba, _ba, random
from bastd.actor.spaz import Spaz
from bastd.actor.scoreboard import Scoreboard
class Player(ba.Player['Team']):
"""Our player type for this game."""
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
super().__init__()
self.score = 1
class ChooseingSpazHitMessage:
def __init__(self, hitter:Player) -> None:
self.hitter = hitter
class ChooseingSpazDieMessage:
def __init__(self, killer:Player) -> None:
self.killer = killer
class ChooseingSpaz(Spaz):
def __init__(
self,
pos:Sequence[float],
color: Sequence[float] = (1.0, 1.0, 1.0),
highlight: Sequence[float] = (0.5, 0.5, 0.5),
):
super().__init__(color, highlight, "Spaz", None, True, True, False, False)
self.last_player_attacked_by = None
self.stand(pos)
self.loc = ba.newnode(
'locator',
attrs={
'shape': 'circleOutline',
'position': pos,
'color': color,
'opacity': 1,
'draw_beauty': False,
'additive': True,
},
)
self.node.connectattr("position", self.loc, "position")
ba.animate_array(self.loc, "size", 1, keys={0:[0.5,], 1:[2,], 1.5:[0.5]}, loop=True)
def handlemessage(self, msg):
if isinstance(msg, ba.FreezeMessage):
return
if isinstance(msg, ba.PowerupMessage):
if not(msg.poweruptype == "health"):
return
super().handlemessage(msg)
if isinstance(msg, ba.HitMessage):
self.handlemessage(ba.PowerupMessage("health"))
player = msg.get_source_player(Player)
if self.is_alive():
self.activity.handlemessage(ChooseingSpazHitMessage(player))
self.last_player_attacked_by = player
elif isinstance(msg, ba.DieMessage):
player = self.last_player_attacked_by
if msg.how.value != ba.DeathType.GENERIC.value:
self._dead = True
self.activity.handlemessage(ChooseingSpazDieMessage(player))
self.loc.delete()
def stand(self, pos = (0,0,0), angle = 0):
self.handlemessage(ba.StandMessage(pos,angle))
def recolor(self, color, highlight = (1,1,1)):
self.node.color = color
self.node.highlight = highlight
self.loc.color = color
class ChooseBilbord(ba.Actor):
def __init__(self, player:Player, delay = 0.1) -> None:
super().__init__()
icon = player.get_icon()
self.scale = 100
self.node = ba.newnode(
'image',
delegate=self,
attrs={
"position":(60,-125),
'texture': icon['texture'],
'tint_texture': icon['tint_texture'],
'tint_color': icon['tint_color'],
'tint2_color': icon['tint2_color'],
'opacity': 1.0,
'absolute_scale': True,
'attach': "topLeft"
},
)
self.name_node = ba.newnode(
'text',
owner=self.node,
attrs={
'position': (60,-185),
'text': ba.Lstr(value=player.getname()),
'color': ba.safecolor(player.team.color),
'h_align': 'center',
'v_align': 'center',
'vr_depth': 410,
'flatness': 1.0,
'h_attach': 'left',
'v_attach': 'top',
'maxwidth':self.scale
},
)
ba.animate_array(self.node, "scale", keys={0 + delay:[0,0], 0.05 + delay:[self.scale, self.scale]}, size=1)
ba.animate(self.name_node, "scale", {0 + delay:0, 0.07 + delay:1})
def handlemessage(self, msg):
super().handlemessage(msg)
if isinstance(msg, ba.DieMessage):
ba.animate_array(self.node, "scale", keys={0:self.node.scale, 0.05:[0,0]}, size=1)
ba.animate(self.name_node, "scale", {0:self.name_node.scale, 0.07:0})
def __delete():
self.node.delete()
self.name_node.delete()
ba.timer(0.2, __delete)
# ba_meta export game
class LastPunchStand(ba.TeamGameActivity[Player, Team]):
name = "Last Punch Stand"
description = "Last one punchs the choosing spaz wins"
tips = [
'keep punching the choosing spaz to be last punched player at times up!',
'you can not frezz the choosing spaz',
"evry time you punch the choosing spaz, you will get one point",
]
default_music = ba.MusicType.TO_THE_DEATH
available_settings = [
ba.FloatSetting("min time limit (in seconds)", 50.0, min_value=30.0),
ba.FloatSetting("max time limit (in seconds)", 160.0, 60),
]
def __init__(self, settings: dict):
super().__init__(settings)
self._min_timelimit = settings["min time limit (in seconds)"]
self._max_timelimit = settings["max time limit (in seconds)"]
if (self._min_timelimit > self._max_timelimit):
self._max_timelimit = self._min_timelimit
self._choosing_spaz_defcolor = (0.5,0.5,0.5)
self.choosing_spaz = None
self.choosed_player = None
self.times_uped = False
self.scoreboard = Scoreboard()
def times_up(self):
self.times_uped = True
for player in self.players:
if self.choosed_player and (player.team.id != self.choosed_player.team.id):
player.actor._cursed = True
player.actor.curse_explode()
self.end_game()
def __get_spaz_bot_spawn_point(self):
if len(self.map.tnt_points) > 0:
return self.map.tnt_points[random.randint(0, len(self.map.tnt_points)-1)]
else:
return (0, 6, 0)
def spaw_bot(self):
"spawns a choosing bot"
self.choosing_spaz = ChooseingSpaz(self.__get_spaz_bot_spawn_point())
self.choose_bilbord = None
def on_begin(self) -> None:
super().on_begin()
time_limit = random.randint(self._min_timelimit, self._max_timelimit)
self.spaw_bot()
ba.timer(time_limit, self.times_up)
self.setup_standard_powerup_drops(False)
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
if self.choosed_player and (team.id == self.choosed_player.team.id): team.score += 100
results.set_team_score(team, team.score)
self.end(results=results)
def change_choosed_player(self, hitter:Player):
if hitter:
self.choosing_spaz.recolor(hitter.color, hitter.highlight)
self.choosed_player = hitter
hitter.team.score += 1
self.choose_bilbord = ChooseBilbord(hitter)
self.hide_score_board()
else:
self.choosing_spaz.recolor(self._choosing_spaz_defcolor)
self.choosed_player = None
self.choose_bilbord = None
self.show_score_board()
def show_score_board(self):
self.scoreboard = Scoreboard()
for team in self.teams:
self.scoreboard.set_team_value(team, team.score)
def hide_score_board(self):
self.scoreboard = None
def _watch_dog_(self):
"checks if choosing spaz exists"
#choosing spaz wont respawn if death type if generic
#this becuse we dont want to keep respawn him when he dies because of losing referce
#but sometimes "choosing spaz" dies naturaly and his death type is generic! so it wont respawn back again
#thats why we have this function; to check if spaz exits in the case that he didnt respawned
if self.choosing_spaz:
if self.choosing_spaz._dead:
self.spaw_bot()
else:
self.spaw_bot()
def handlemessage(self, msg):
super().handlemessage(msg)
if isinstance(msg, ChooseingSpazHitMessage):
hitter = msg.hitter
if self.choosing_spaz.node and hitter:
self.change_choosed_player(hitter)
elif isinstance(msg, ChooseingSpazDieMessage):
self.spaw_bot()
self.change_choosed_player(None)
elif isinstance(msg, ba.PlayerDiedMessage):
player = msg.getplayer(Player)
if not (self.has_ended() or self.times_uped):
self.respawn_player(player, 0)
if self.choosed_player and (player.getname(True) == self.choosed_player.getname(True)):
self.change_choosed_player(None)
self._watch_dog_()

642
plugins/minigames/quake.py Normal file
View file

@ -0,0 +1,642 @@
"""Quake Game Activity"""
# ba_meta require api 7
from __future__ import annotations
from typing import TYPE_CHECKING
import random
import enum
import ba, _ba
from bastd.actor.scoreboard import Scoreboard
from bastd.actor.powerupbox import PowerupBox
from bastd.gameutils import SharedObjects
#from rocket
from bastd.actor.bomb import Blast
#from railgun
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.spaz import Spaz
if TYPE_CHECKING:
from typing import Optional, List, Any, Type, Union, Sequence
STORAGE_ATTR_NAME = f'_shared_{__name__}_factory'
#+++++++++++++++++++Rocket++++++++++++++++++++++++
class RocketFactory:
"""Quake Rocket factory"""
def __init__(self) -> None:
self.ball_material = ba.Material()
self.ball_material.add_actions(
conditions=((('we_are_younger_than', 5), 'or',
('they_are_younger_than', 5)), 'and',
('they_have_material',
SharedObjects.get().object_material)),
actions=('modify_node_collision', 'collide', False))
self.ball_material.add_actions(
conditions=('they_have_material',
SharedObjects.get().pickup_material),
actions=('modify_part_collision', 'use_node_collide', False))
self.ball_material.add_actions(actions=('modify_part_collision',
'friction', 0))
self.ball_material.add_actions(
conditions=(('they_have_material',
SharedObjects.get().footing_material), 'or',
('they_have_material',
SharedObjects.get().object_material)),
actions=('message', 'our_node', 'at_connect', ImpactMessage()))
@classmethod
def get(cls):
"""Get factory if exists else create new"""
activity = ba.getactivity()
if hasattr(activity, STORAGE_ATTR_NAME):
return getattr(activity, STORAGE_ATTR_NAME)
factory = cls()
setattr(activity, STORAGE_ATTR_NAME, factory)
return factory
class RocketLauncher:
"""Very dangerous weapon"""
def __init__(self):
self.last_shot: Optional[int, float] = 0
def give(self, spaz: Spaz) -> None:
"""Give spaz a rocket launcher"""
spaz.punch_callback = self.shot
self.last_shot = ba.time()
# FIXME
# noinspection PyUnresolvedReferences
def shot(self, spaz: Spaz) -> None:
"""Release a rocket"""
time = ba.time()
if time - self.last_shot > 0.6:
self.last_shot = time
center = spaz.node.position_center
forward = spaz.node.position_forward
direction = [center[0] - forward[0], forward[1] - center[1],
center[2] - forward[2]]
direction[1] = 0.0
mag = 10.0 / ba.Vec3(*direction).length()
vel = [v * mag for v in direction]
Rocket(position=spaz.node.position,
velocity=vel,
owner=spaz.getplayer(ba.Player),
source_player=spaz.getplayer(ba.Player),
color=spaz.node.color).autoretain()
class ImpactMessage:
"""Rocket touched something"""
class Rocket(ba.Actor):
"""Epic rocket from rocket launcher"""
def __init__(self,
position=(0, 5, 0),
velocity=(1, 0, 0),
source_player=None,
owner=None,
color=(1.0, 0.2, 0.2)) -> None:
super().__init__()
self.source_player = source_player
self.owner = owner
self._color = color
factory = RocketFactory.get()
self.node = ba.newnode('prop',
delegate=self,
attrs={
'position': position,
'velocity': velocity,
'model': ba.getmodel('impactBomb'),
'body': 'sphere',
'color_texture': ba.gettexture(
'bunnyColor'),
'model_scale': 0.2,
'is_area_of_interest': True,
'body_scale': 0.8,
'materials': [
SharedObjects.get().object_material,
factory.ball_material]
}) # yapf: disable
self.node.extra_acceleration = (self.node.velocity[0] * 200, 0,
self.node.velocity[2] * 200)
self._life_timer = ba.Timer(
5, ba.WeakCall(self.handlemessage, ba.DieMessage()))
self._emit_timer = ba.Timer(0.001, ba.WeakCall(self.emit), repeat=True)
self.base_pos_y = self.node.position[1]
ba.camerashake(5.0)
def emit(self) -> None:
"""Emit a trace after rocket"""
ba.emitfx(position=self.node.position,
scale=0.4,
spread=0.01,
chunk_type='spark')
if not self.node:
return
self.node.position = (self.node.position[0], self.base_pos_y,
self.node.position[2]) # ignore y
ba.newnode('explosion',
owner=self.node,
attrs={
'position': self.node.position,
'radius': 0.2,
'color': self._color
})
def handlemessage(self, msg: Any) -> Any:
"""Message handling for rocket"""
super().handlemessage(msg)
if isinstance(msg, ImpactMessage):
self.node.handlemessage(ba.DieMessage())
elif isinstance(msg, ba.DieMessage):
if self.node:
Blast(position=self.node.position,
blast_radius=2,
source_player=self.source_player)
self.node.delete()
self._emit_timer = None
elif isinstance(msg, ba.OutOfBoundsMessage):
self.handlemessage(ba.DieMessage())
#-------------------Rocket--------------------------
#++++++++++++++++++Railgun++++++++++++++++++++++++++
class Railgun:
"""Very dangerous weapon"""
def __init__(self) -> None:
self.last_shot: Optional[int, float] = 0
def give(self, spaz: Spaz) -> None:
"""Give spaz a railgun"""
spaz.punch_callback = self.shot
self.last_shot = ba.time()
# FIXME
# noinspection PyUnresolvedReferences
def shot(self, spaz: Spaz) -> None:
"""Release a rocket"""
time = ba.time()
if time - self.last_shot > 0.6:
self.last_shot = time
center = spaz.node.position_center
forward = spaz.node.position_forward
direction = [
center[0] - forward[0], forward[1] - center[1],
center[2] - forward[2]
]
direction[1] = 0.0
RailBullet(position=spaz.node.position,
direction=direction,
owner=spaz.getplayer(ba.Player),
source_player=spaz.getplayer(ba.Player),
color=spaz.node.color).autoretain()
class TouchedToSpazMessage:
"""I hit!"""
def __init__(self, spaz) -> None:
self.spaz = spaz
class RailBullet(ba.Actor):
"""Railgun bullet"""
def __init__(self,
position=(0, 5, 0),
direction=(0, 2, 0),
source_player=None,
owner=None,
color=(1, 1, 1)) -> None:
super().__init__()
self._color = color
self.node = ba.newnode('light',
delegate=self,
attrs={
'position': position,
'color': self._color
})
ba.animate(self.node, 'radius', {0: 0, 0.1: 0.5, 0.5: 0})
self.source_player = source_player
self.owner = owner
self._life_timer = ba.Timer(
0.5, ba.WeakCall(self.handlemessage, ba.DieMessage()))
pos = position
vel = tuple(i / 5 for i in ba.Vec3(direction).normalized())
for _ in range(500): # Optimization :(
ba.newnode('explosion',
owner=self.node,
attrs={
'position': pos,
'radius': 0.2,
'color': self._color
})
pos = (pos[0] + vel[0], pos[1] + vel[1], pos[2] + vel[2])
for node in _ba.getnodes():
if node and node.getnodetype() == 'spaz':
# pylint: disable=invalid-name
m3 = ba.Vec3(position)
a = ba.Vec3(direction[2], direction[1], direction[0])
m1 = ba.Vec3(node.position)
# pylint: enable=invalid-name
# distance between node and line
dist = (a * (m1 - m3)).length() / a.length()
if dist < 0.3:
if node and node != self.owner and node.getdelegate(
PlayerSpaz, True).getplayer(
ba.Player, True).team != self.owner.team:
node.handlemessage(ba.FreezeMessage())
pos = self.node.position
hit_dir = (0, 10, 0)
node.handlemessage(
ba.HitMessage(pos=pos,
magnitude=50,
velocity_magnitude=50,
radius=0,
srcnode=self.node,
source_player=self.source_player,
force_direction=hit_dir))
def handlemessage(self, msg: Any) -> Any:
super().handlemessage(msg)
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
elif isinstance(msg, ba.OutOfBoundsMessage):
self.handlemessage(ba.DieMessage())
#------------------Railgun-------------------------
class Player(ba.Player['Team']):
"""Our player"""
class Team(ba.Team[Player]):
"""Our team"""
def __init__(self) -> None:
self.score = 0
class WeaponType(enum.Enum):
"""Type of weapon"""
ROCKET = 0
RAILGUN = 1
class ObstaclesForm(enum.Enum):
"""Obstacle form"""
CUBE = 0
SPHERE = 1
RANDOM = 2
# ba_meta export game
class QuakeGame(ba.TeamGameActivity[Player, Team]):
"""Quake Team Game Activity"""
name = 'Quake'
description = 'Kill a set number of enemies to win.'
available_settings = [
ba.IntSetting(
'Kills to Win Per Player',
default=15,
min_value=1,
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=[('At once', 0.0), ('Shorter', 0.25), ('Short', 0.5),
('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)],
default=1.0,
),
ba.BoolSetting(
'Speed',
default=True,
),
ba.BoolSetting(
'Enable Jump',
default=True,
),
ba.BoolSetting(
'Enable Pickup',
default=True,
),
ba.BoolSetting(
'Enable Bomb',
default=False,
),
ba.BoolSetting(
'Obstacles',
default=True,
),
ba.IntChoiceSetting(
'Obstacles Form',
choices=[('Cube', ObstaclesForm.CUBE.value),
('Sphere', ObstaclesForm.SPHERE.value),
('Random', ObstaclesForm.RANDOM.value)],
default=0,
),
ba.IntChoiceSetting(
'Weapon Type',
choices=[('Rocket', WeaponType.ROCKET.value),
('Railgun', WeaponType.RAILGUN.value)],
default=WeaponType.ROCKET.value,
),
ba.BoolSetting(
'Obstacles Mirror Shots',
default=False,
),
ba.IntSetting(
'Obstacles Count',
default=16,
min_value=0,
increment=2,
),
ba.BoolSetting(
'Random Obstacles Color',
default=True,
),
ba.BoolSetting(
'Epic Mode',
default=False,
),
]
@classmethod
def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool:
return issubclass(sessiontype, ba.MultiTeamSession) or issubclass(
sessiontype, ba.FreeForAllSession)
@classmethod
def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]:
# TODO add more maps
return ['Football Stadium', 'Monkey Face', 'Doom Shroom']
def __init__(self, settings) -> None:
super().__init__(settings)
self._epic_mode = self.settings_raw['Epic Mode']
self._score_to_win = self.settings_raw['Kills to Win Per Player']
self._time_limit = self.settings_raw['Time Limit']
self._obstacles_enabled = self.settings_raw['Obstacles']
self._obstacles_count = self.settings_raw['Obstacles Count']
self._speed_enabled = self.settings_raw['Speed']
self._bomb_enabled = self.settings_raw['Enable Bomb']
self._pickup_enabled = self.settings_raw['Enable Pickup']
self._jump_enabled = self.settings_raw['Enable Jump']
self._weapon_type = WeaponType(self.settings_raw['Weapon Type'])
self.default_music = (ba.MusicType.EPIC
if self._epic_mode else ba.MusicType.GRAND_ROMP)
self.slow_motion = self._epic_mode
self.announce_player_deaths = True
self._scoreboard = Scoreboard()
self._ding_sound = ba.getsound('dingSmall')
self._shield_dropper: Optional[ba.Timer] = None
def get_instance_description(self) -> Union[str, Sequence]:
return 'Kill ${ARG1} enemies.', self._score_to_win
def on_team_join(self, team: Team) -> None:
team.score = 0
if self.has_begun():
self._update_scoreboard()
def on_begin(self) -> None:
ba.TeamGameActivity.on_begin(self)
ba.getactivity().globalsnode.tint = (0.5, 0.7, 1)
self.drop_shield()
self._shield_dropper = ba.Timer(8,
ba.WeakCall(self.drop_shield),
repeat=True)
self.setup_standard_time_limit(self._time_limit)
if self._obstacles_enabled:
count = self._obstacles_count
gamemap = self.map.getname()
for i in range(count): # TODO: tidy up around here
if gamemap == 'Football Stadium':
radius = (random.uniform(-10, 1),
6,
random.uniform(-4.5, 4.5)) \
if i > count / 2 else (
random.uniform(10, 1), 6, random.uniform(-4.5, 4.5))
else:
radius = (random.uniform(-10, 1),
6,
random.uniform(-8, 8)) \
if i > count / 2 else (
random.uniform(10, 1), 6, random.uniform(-8, 8))
Obstacle(
position=radius,
mirror=self.settings_raw['Obstacles Mirror Shots'],
form=self.settings_raw['Obstacles Form']).autoretain()
self._update_scoreboard()
def drop_shield(self) -> None:
"""Drop a shield powerup in random place"""
# FIXME: should use map defs
shield = PowerupBox(poweruptype='shield',
position=(random.uniform(-10, 10), 6,
random.uniform(-5, 5))).autoretain()
ba.playsound(self._ding_sound)
p_light = ba.newnode('light',
owner=shield.node,
attrs={
'position': (0, 0, 0),
'color': (0.3, 0.0, 0.4),
'radius': 0.3,
'intensity': 2,
'volume_intensity_scale': 10.0
})
shield.node.connectattr('position', p_light, 'position')
ba.animate(p_light, 'intensity', {0: 2, 8: 0})
def spawn_player(self, player: Player) -> None:
spaz = self.spawn_player_spaz(player)
if self._weapon_type == WeaponType.ROCKET:
RocketLauncher().give(spaz)
elif self._weapon_type == WeaponType.RAILGUN:
Railgun().give(spaz)
spaz.connect_controls_to_player(enable_jump=self._jump_enabled,
enable_pickup=self._pickup_enabled,
enable_bomb=self._bomb_enabled,
enable_fly=False)
spaz.node.hockey = self._speed_enabled
spaz.spaz_light = ba.newnode('light',
owner=spaz.node,
attrs={
'position': (0, 0, 0),
'color': spaz.node.color,
'radius': 0.12,
'intensity': 1,
'volume_intensity_scale': 10.0
})
spaz.node.connectattr('position', spaz.spaz_light, 'position')
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
ba.TeamGameActivity.handlemessage(self, msg)
player = msg.getplayer(Player)
self.respawn_player(player)
killer = msg.getkillerplayer(Player)
if killer is None:
return
# handle team-kills
if killer.team is player.team:
# in free-for-all, killing yourself loses you a point
if isinstance(self.session, ba.FreeForAllSession):
new_score = player.team.score - 1
new_score = max(0, new_score)
player.team.score = new_score
# in teams-mode it gives a point to the other team
else:
ba.playsound(self._ding_sound)
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
ba.playsound(self._ding_sound)
# in FFA show our score since its hard to find on
# the scoreboard
assert killer.actor is not None
# noinspection PyUnresolvedReferences
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)
if any(team.score >= self._score_to_win for team in self.teams):
ba.timer(0.5, self.end_game)
else:
ba.TeamGameActivity.handlemessage(self, msg)
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 = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.score)
self.end(results=results)
class Obstacle(ba.Actor):
"""Scene object"""
def __init__(self,
position,
form=ObstaclesForm.CUBE,
mirror=False) -> None:
ba.Actor.__init__(self)
if form == ObstaclesForm.CUBE:
model = 'tnt'
body = 'crate'
elif form == ObstaclesForm.SPHERE:
model = 'bomb'
body = 'sphere'
else: # ObstaclesForm.RANDOM:
model = random.choice(['tnt', 'bomb'])
body = 'sphere' if model == 'bomb' else 'crate'
self.node = ba.newnode(
'prop',
delegate=self,
attrs={
'position':
position,
'model':
ba.getmodel(model),
'body':
body,
'body_scale':
1.3,
'model_scale':
1.3,
'reflection':
'powerup',
'reflection_scale': [0.7],
'color_texture':
ba.gettexture('bunnyColor'),
'materials': [SharedObjects.get().footing_material]
if mirror else [
SharedObjects.get().object_material,
SharedObjects.get().footing_material
]
})
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
elif isinstance(msg, ba.OutOfBoundsMessage):
if self.node:
self.handlemessage(ba.DieMessage())
elif isinstance(msg, ba.HitMessage):
self.node.handlemessage('impulse', msg.pos[0], msg.pos[1],
msg.pos[2], msg.velocity[0],
msg.velocity[1], msg.velocity[2],
msg.magnitude, msg.velocity_magnitude,
msg.radius, 0, msg.velocity[0],
msg.velocity[1], msg.velocity[2])

View file

@ -0,0 +1,783 @@
# Released under the MIT License. See LICENSE for details.
# y me (: itsre3
# =>2<=
#BCS RULES
#
"""Defines Race mini-game."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import random
from typing import TYPE_CHECKING
from dataclasses import dataclass
import ba, _ba
from bastd.actor.bomb import Bomb
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict,
Union)
from bastd.actor.onscreentimer import OnScreenTimer
@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]
})
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> 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) -> None:
self.time: Optional[float] = None
self.lap = 0
self.finished = False
# ba_meta export game
class SleepRaceGame(ba.TeamGameActivity[Player, Team]):
"""Game of racing around a track."""
name = 'Sleep Race'
description = 'Can you run while sleeping?'
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=2, 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.IntChoiceSetting(
'Knockout Time',
choices=[
('8 Seconds', 8000),
('5 Seconds', 5000),
],
default=5000,
),
ba.BoolSetting('Epic Mode', default=False),
ba.BoolSetting('Credits', default=True),
]
# 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)
self._scoreboard = Scoreboard()
self._score_sound = ba.getsound('score')
self._swipsound = ba.getsound('swip')
self._last_team_time: Optional[float] = None
self._front_race_region: Optional[int] = None
self._nub_tex = ba.gettexture('nub')
self._beep_1_sound = ba.getsound('raceBeep1')
self._beep_2_sound = ba.getsound('raceBeep2')
self.race_region_material: Optional[ba.Material] = None
self._regions: List[RaceRegion] = []
self._team_finish_pts: Optional[int] = None
self._time_text: Optional[ba.Actor] = None
self._cd_text: Optional[ba.Actor] = None
self._timer: Optional[OnScreenTimer] = None
self._race_mines: Optional[List[RaceMine]] = None
self._race_mine_timer: Optional[ba.Timer] = None
self._scoreboard_timer: Optional[ba.Timer] = None
self._player_order_update_timer: Optional[ba.Timer] = None
self._start_lights: Optional[List[ba.Node]] = None
self._bomb_spawn_timer: Optional[ba.Timer] = None
self._knockout_timer: Optional[ba.Timer] = 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._knockout_time = float(settings['Knockout Time'])
self._epic_mode = bool(settings['Epic Mode'])
self._credits = bool(settings['Credits'])
# Base class overrides.
self.slow_motion = self._epic_mode
self.default_music = (ba.MusicType.EPIC_RACE
if self._epic_mode else ba.MusicType.RACE)
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_transition_in(self) -> None:
super().on_transition_in()
shared = SharedObjects.get()
pts = self.map.get_def_points('race_point')
mat = self.race_region_material = ba.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 = ba.newnode('light',
attrs={
'position': pos,
'color': (1, 1, 0),
'height_attenuated': False,
'radius': 0.4
})
ba.timer(0.5, light.delete)
ba.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 = ba.getcollision()
try:
region = collision.sourcenode.getdelegate(RaceRegion, True)
player = collision.opposingnode.getdelegate(PlayerSpaz,
True).getplayer(
Player, True)
except ba.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(ba.DieMessage())
ba.screenmessage(ba.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, ba.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, ba.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(
ba.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:
ba.playsound(self._score_sound)
player.team.finished = True
assert self._timer is not None
elapsed = ba.time() - self._timer.getstarttime()
self._last_team_time = player.team.time = elapsed
self._check_end_game()
# Team has yet to finish.
else:
ba.playsound(self._swipsound)
# They've just finished a lap but not the race.
else:
ba.playsound(self._swipsound)
self._flash_player(player, 0.3)
# Print their lap number over their head.
try:
assert isinstance(player.actor, PlayerSpaz)
mathnode = ba.newnode('math',
owner=player.actor.node,
attrs={
'input1': (0, 1.9, 0),
'operation': 'add'
})
player.actor.node.connectattr(
'torso_position', mathnode, 'input2')
tstr = ba.Lstr(resource='lapNumberText',
subs=[('${CURRENT}',
str(player.lap + 1)),
('${TOTAL}', str(self._laps))
])
txtnode = ba.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')
ba.animate(txtnode, 'scale', {
0.0: 0,
0.2: 0.019,
2.0: 0.019,
2.2: 0
})
ba.timer(2.3, mathnode.delete)
except Exception:
ba.print_exception('Error printing lap.')
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
def on_player_leave(self, player: Player) -> None:
super().on_player_leave(player)
# A player leaving disqualifies the team if 'Entire Team Must Finish'
# is on (otherwise in teams mode everyone could just leave except the
# leading player to win).
if (isinstance(self.session, ba.DualTeamSession)
and self._entire_team_must_finish):
ba.screenmessage(ba.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
ba.playsound(ba.getsound('boo'))
for otherplayer in player.team.players:
otherplayer.lap = 0
otherplayer.finished = True
try:
if otherplayer.actor is not None:
otherplayer.actor.handlemessage(ba.DieMessage())
except Exception:
ba.print_exception('Error sending DieMessage.')
# Defer so team/player lists will be updated.
ba.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, ba.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 bastd.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
if self._credits:
self._cd_text = ba.NodeActor(
ba.newnode('text',
attrs={
'position': (0, 0),
'h_attach': 'center',
'h_align': 'center',
'maxwidth': 200,
'shadow': 0.5,
'vr_depth': 390,
'scale': 0.6,
'v_attach': 'bottom',
'color': (1, 1, 1),
'text': 'By itsre3'
}))
# Throw a timer up on-screen.
self._time_text = ba.NodeActor(
ba.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 = ba.Timer(0.001 * self._mine_spawning,
self._update_race_mine,
repeat=True)
self._scoreboard_timer = ba.Timer(0.25,
self._update_scoreboard,
repeat=True)
self._player_order_update_timer = ba.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
ba.timer(lstart, self._do_light_1)
ba.timer(lstart + inc, self._do_light_2)
ba.timer(lstart + 2 * inc, self._do_light_3)
ba.timer(lstart + 3 * inc, self._start_race)
self._start_lights = []
for i in range(4):
lnub = ba.newnode('image',
attrs={
'texture': ba.gettexture('nub'),
'opacity': 1.0,
'absolute_scale': True,
'position': (-75 + i * 50, light_y),
'scale': (50, 50),
'attach': 'center'
})
ba.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
})
ba.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)
ba.playsound(self._beep_1_sound)
def _do_light_2(self) -> None:
assert self._start_lights is not None
self._start_lights[1].color = (1.0, 0, 0)
ba.playsound(self._beep_1_sound)
def _do_light_3(self) -> None:
assert self._start_lights is not None
self._start_lights[2].color = (1.0, 0.3, 0)
ba.playsound(self._beep_1_sound)
def _start_race(self) -> None:
assert self._start_lights is not None
self._start_lights[3].color = (0.0, 1.0, 0)
ba.playsound(self._beep_2_sound)
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:
ba.print_exception('Error in race player connects.')
assert self._timer is not None
self._timer.start()
if self._bomb_spawning != 0:
self._bomb_spawn_timer = ba.Timer(0.001 * self._bomb_spawning,
self._spawn_bomb,
repeat=True)
def knock_players():
activity = _ba.get_foreground_host_activity()
gnode = ba.getactivity().globalsnode
for players in activity.players:
gnode.tint = (0.5,0.5,0.5)
node = players.actor.node
node.handlemessage('knockout', 600.0)
self.text_offset = ba.newnode('math',
owner=node,
attrs={'input1': (-0.5, 0.5, 0.25),
'operation': 'add'})
node.connectattr(
'torso_position',
self.text_offset,
'input2')
self.text = ba.newnode('text',
owner=node,
attrs={
'h_align': 'right',
'color': (1.0, 1.0, 1.0),
'shadow': 1.0,
'text': 'z z',
'scale': 0.01,
'in_world': True})
self.text_offset.connectattr(
'output',
self.text,
'position')
ba.animate(self.text, 'scale', {0: 0.0, 1.0: 0.01})
ba.timer(2, self.text.delete)
if self._knockout_time != 0:
knock_time = 0.001 * self._knockout_time
self._knockout_timer = ba.Timer(knock_time,
knock_players,
repeat=True)
self._race_started = True
def _update_player_order(self) -> None:
# Calc all player distances.
for player in self.players:
pos: Optional[ba.Vec3]
try:
pos = player.position
except ba.NotFoundError:
pos = None
if pos is not None:
r_index = player.last_region
rg1 = self._regions[r_index]
r1pt = ba.Vec3(rg1.pos[:3])
rg2 = self._regions[0] if r_index == len(
self._regions) - 1 else self._regions[r_index + 1]
r2pt = ba.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))
ba.timer(random.uniform(0.0, 2.0),
ba.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 = ba.newnode('light',
attrs={
'position': rmine.point[:3],
'color': (1, 0.2, 0.2),
'radius': 0.1,
'height_attenuated': False
})
ba.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True)
ba.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)
ba.timer(0.95, ba.Call(self._make_mine, m_index))
def spawn_player(self, player: Player) -> ba.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 = ba.newnode('math',
owner=spaz.node,
attrs={
'input1': (0, 1.4, 0),
'operation': 'add'
})
spaz.node.connectattr('torso_position', mathnode, 'input2')
distance_txt = ba.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')
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, ba.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, ba.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 = ba.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,
ba.DualTeamSession))
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.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)

314
plugins/minigames/snake.py Normal file
View file

@ -0,0 +1,314 @@
#snake
# Released under the MIT License. See LICENSE for details.
#
"""Snake game by SEBASTIAN2059"""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.actor import bomb as stdbomb
if TYPE_CHECKING:
from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional
class ScoreMessage:
"""It will help us with the scores."""
def __init__(self,player: Player):
self.player = player
def getplayer(self):
return self.player
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
self.mines = []
self.actived = None
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.score = 0
lang = ba.app.lang.language
if lang == 'Spanish':
description = 'Sobrevive a un número determinado de minas para ganar.'
join_description = 'Corre y no te dejes matar.'
view_description = 'sobrevive ${ARG1} minas'
else:
description = 'Survive a set number of mines to win.'
join_description = "Run and don't get killed."
view_description = 'survive ${ARG1} mines'
class Custom_Mine(stdbomb.Bomb):
"""Custom a mine :)"""
def __init__(self,position,source_player):
stdbomb.Bomb.__init__(self,position=position,bomb_type='land_mine',source_player=source_player)
def handlemessage(self,msg: Any) -> Any:
if isinstance(msg, ba.HitMessage):
return
else:
super().handlemessage(msg)
# ba_meta export game
class SnakeGame(ba.TeamGameActivity[Player, Team]):
"""A game type based on acquiring kills."""
name = 'Snake'
description = description
# 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(
'Score to Win',
min_value=40,
default=80,
increment=5,
),
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),
]
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)
self._scoreboard = Scoreboard()
self._score_to_win: Optional[int] = None
self._dingsound = ba.getsound('dingSmall')
self._beep_1_sound = ba.getsound('raceBeep1')
self._beep_2_sound = ba.getsound('raceBeep2')
self._epic_mode = bool(settings['Epic Mode'])
self._kills_to_win_per_player = int(
settings['Score to Win'])
self._time_limit = float(settings['Time Limit'])
self._started = False
# Base class overrides.
self.slow_motion = self._epic_mode
self.default_music = (ba.MusicType.EPIC if self._epic_mode else
ba.MusicType.TO_THE_DEATH)
def get_instance_description(self) -> Union[str, Sequence]:
return join_description
def get_instance_description_short(self) -> Union[str, Sequence]:
return view_description, 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)))
self._update_scoreboard()
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
ba.timer(lstart, self._do_light_1)
ba.timer(lstart + inc, self._do_light_2)
ba.timer(lstart + 2 * inc, self._do_light_3)
ba.timer(lstart + 3 * inc, self._start_race)
self._start_lights = []
for i in range(4):
lnub = ba.newnode('image',
attrs={
'texture': ba.gettexture('nub'),
'opacity': 1.0,
'absolute_scale': True,
'position': (-75 + i * 50, light_y),
'scale': (50, 50),
'attach': 'center'
})
ba.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
})
ba.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)
ba.playsound(self._beep_1_sound)
def _do_light_2(self) -> None:
assert self._start_lights is not None
self._start_lights[1].color = (1.0, 0, 0)
ba.playsound(self._beep_1_sound)
def _do_light_3(self) -> None:
assert self._start_lights is not None
self._start_lights[2].color = (1.0, 0.3, 0)
ba.playsound(self._beep_1_sound)
def _start_race(self) -> None:
assert self._start_lights is not None
self._start_lights[3].color = (0.0, 1.0, 0)
ba.playsound(self._beep_2_sound)
self._started = True
for player in self.players:
self.generate_mines(player)
# overriding the default character spawning..
def spawn_player(self, player: Player) -> ba.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
if self._started:
self.generate_mines(player)
return spaz
def generate_mines(self,player: Player):
try:
player.actived = ba.Timer(0.5,ba.Call(self.spawn_mine, player),repeat=True)
except Exception as e:
print('Exception -> '+ str(e))
def spawn_mine(self,player: Player):
if player.team.score >= self._score_to_win:
return
pos = player.actor.node.position
# mine = stdbomb.Bomb(position=(pos[0], pos[1] + 2.0, pos[2]),
# velocity=(0, 0, 0),
# bomb_type='land_mine',
# #blast_radius=,
# source_player=player.actor.source_player,
# owner=player.actor.node).autoretain()
mine = Custom_Mine(position=(pos[0], pos[1] + 2.0, pos[2]),
source_player=player.actor.source_player)
def arm():
mine.arm()
ba.timer(0.5,arm)
player.mines.append(mine)
if len(player.mines) > 15:
for m in player.mines:
try:
m.node.delete()
except Exception:
pass
player.mines.remove(m)
break
self.handlemessage(ScoreMessage(player))
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
player = msg.getplayer(Player)
self.respawn_player(player)
player.actived = None
elif isinstance(msg, ScoreMessage):
player = msg.getplayer()
player.team.score += 1
self._update_scoreboard()
assert self._score_to_win is not None
if any(team.score >= self._score_to_win for team in self.teams):
self.end_game() #ba.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 = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.score)
self.end(results=results)

View file

@ -0,0 +1,994 @@
"""UFO Boss Fight v1.0:
Made by Cross Joy"""
# Anyone who wanna help me in giving suggestion/ fix bugs/ by creating PR,
# Can visit my github https://github.com/CrossJoy/Bombsquad-Modding
# You can contact me through discord:
# My Discord Id: Cross Joy#0721
# My BS Discord Server: https://discford.gg/JyBY6haARJ
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba, _ba
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.spaz import Spaz
from bastd.actor.bomb import Blast, Bomb
from bastd.actor.onscreentimer import OnScreenTimer
from bastd.actor.spazbot import SpazBotSet, StickyBot
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence, Union, Callable
class UFODiedMessage:
ufo: UFO
"""The UFO that was killed."""
killerplayer: ba.Player | None
"""The ba.Player that killed it (or None)."""
how: ba.DeathType
"""The particular type of death."""
def __init__(
self,
ufo: UFO,
killerplayer: ba.Player | None,
how: ba.DeathType,
):
"""Instantiate with given values."""
self.spazbot = ufo
self.killerplayer = killerplayer
self.how = how
class RoboBot(StickyBot):
character = 'B-9000'
default_bomb_type = 'land_mine'
color = (0, 0, 0)
highlight = (3, 3, 3)
class UFO(ba.Actor):
"""
New AI for Boss
"""
# pylint: disable=too-many-public-methods
# pylint: disable=too-many-locals
node: ba.Node
def __init__(self, hitpoints: int = 5000):
super().__init__()
shared = SharedObjects.get()
self.update_callback: Callable[[UFO], Any] | None = None
activity = self.activity
assert isinstance(activity, ba.GameActivity)
self.platform_material = ba.Material()
self.platform_material.add_actions(
conditions=('they_have_material', shared.footing_material),
actions=(
'modify_part_collision', 'collide', True))
self.ice_material = ba.Material()
self.ice_material.add_actions(
actions=('modify_part_collision', 'friction', 0.0))
self._player_pts: list[tuple[ba.Vec3, ba.Vec3]] | None = None
self._ufo_update_timer: ba.Timer | None = None
self.last_player_attacked_by: ba.Player | None = None
self.last_attacked_time = 0.0
self.last_attacked_type: tuple[str, str] | None = None
self.to_target: ba.Vec3 = ba.Vec3(0, 0, 0)
self.dist = (0, 0, 0)
self._bots = SpazBotSet()
self.frozen = False
self.y_pos = 3
self.xz_pos = 1
self.bot_count = 3
self.bot_dur_froze = False
self.hitpoints = hitpoints
self.hitpoints_max = hitpoints
self._width = 240
self._width_max = 240
self._height = 35
self._bar_width = 240
self._bar_height = 35
self._bar_tex = self._backing_tex = ba.gettexture('bar')
self._cover_tex = ba.gettexture('uiAtlas')
self._model = ba.getmodel('meterTransparent')
self.bar_posx = -120
self._last_hit_time: int | None = None
self.impact_scale = 1.0
self._num_times_hit = 0
self._sucker_mat = ba.Material()
self.ufo_material = ba.Material()
self.ufo_material.add_actions(
conditions=('they_have_material',
shared.player_material),
actions=(('modify_node_collision', 'collide', True),
('modify_part_collision', 'physical', True)))
self.ufo_material.add_actions(
conditions=(('they_have_material',
shared.object_material), 'or',
('they_have_material',
shared.footing_material), 'or',
('they_have_material',
self.ufo_material)),
actions=('modify_part_collision', 'physical', False))
activity = _ba.get_foreground_host_activity()
with ba.Context(activity):
point = activity.map.get_flag_position(None)
boss_spawn_pos = (point[0], point[1] + 1, point[2])
self.node = ba.newnode('prop', delegate=self, attrs={
'position': boss_spawn_pos,
'velocity': (2, 0, 0),
'color_texture': ba.gettexture('achievementFootballShutout'),
'model': ba.getmodel('landMine'),
# 'light_model': ba.getmodel('powerupSimple'),
'model_scale': 3.3,
'body': 'landMine',
'body_scale': 3.3,
'gravity_scale': 0.05,
'density': 1,
'reflection': 'soft',
'reflection_scale': [0.25],
'shadow_size': 0.1,
'max_speed': 1.5,
'is_area_of_interest':
True,
'materials': [shared.footing_material, shared.object_material]})
self.holder = ba.newnode('region', attrs={
'position': (
boss_spawn_pos[0], boss_spawn_pos[1] - 0.25,
boss_spawn_pos[2]),
'scale': [6, 0.1, 2.5 - 0.1],
'type': 'box',
'materials': (self.platform_material, self.ice_material,
shared.object_material)})
self.suck_anim = ba.newnode('locator',
owner=self.node,
attrs={'shape': 'circleOutline',
'position': (
boss_spawn_pos[0],
boss_spawn_pos[1] - 0.25,
boss_spawn_pos[2]),
'color': (4, 4, 4),
'opacity': 1.0,
'draw_beauty': True,
'additive': True})
def suck_anim():
ba.animate_array(self.suck_anim, 'position', 3,
{0: (
self.node.position[0],
self.node.position[1] - 5,
self.node.position[2]),
0.5: (
self.node.position[
0] + self.to_target.x / 2,
self.node.position[
1] + self.to_target.y / 2,
self.node.position[
2] + self.to_target.z / 2)})
self.suck_timer = ba.Timer(0.5, suck_anim, repeat=True)
self.blocks = []
self._sucker_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._levitate)
))
# self.sucker = ba.newnode('region', attrs={
# 'position': (
# boss_spawn_pos[0], boss_spawn_pos[1] - 2, boss_spawn_pos[2]),
# 'scale': [2, 10, 2],
# 'type': 'box',
# 'materials': self._sucker_mat, })
self.suck = ba.newnode('region',
attrs={'position': (
boss_spawn_pos[0], boss_spawn_pos[1] - 2,
boss_spawn_pos[2]),
'scale': [1, 10, 1],
'type': 'box',
'materials': [self._sucker_mat]})
self.node.connectattr('position', self.holder, 'position')
self.node.connectattr('position', self.suck, 'position')
ba.animate(self.node, 'model_scale', {
0: 0,
0.2: self.node.model_scale * 1.1,
0.26: self.node.model_scale})
self.shield_deco = ba.newnode('shield', owner=self.node,
attrs={'color': (4, 4, 4),
'radius': 1.2})
self.node.connectattr('position', self.shield_deco, 'position')
self._scoreboard()
self._update()
self.drop_bomb_timer = ba.Timer(1.5, ba.Call(self._drop_bomb),
repeat=True)
self.drop_bots_timer = ba.Timer(15.0, ba.Call(self._drop_bots), repeat=True)
def _drop_bots(self) -> None:
p = self.node.position
if not self.frozen:
for i in range(self.bot_count):
ba.timer(
1.0 + i,
lambda: self._bots.spawn_bot(
RoboBot, pos=(self.node.position[0],
self.node.position[1] - 1,
self.node.position[2]), spawn_time=0.0
),
)
else:
self.bot_dur_froze = True
def _drop_bomb(self) -> None:
t = self.to_target
p = self.node.position
if not self.frozen:
if abs(self.dist[0]) < 2 and abs(self.dist[2]) < 2:
Bomb(position=(p[0], p[1] - 0.5, p[2]),
velocity=(t[0] * 5, 0, t[2] * 5),
bomb_type='land_mine').autoretain().arm()
elif self.hitpoints > self.hitpoints_max * 3 / 4:
Bomb(position=(p[0], p[1] - 1.5, p[2]),
velocity=(t[0] * 8, 2, t[2] * 8),
bomb_type='normal').autoretain()
elif self.hitpoints > self.hitpoints_max * 1 / 2:
Bomb(position=(p[0], p[1] - 1.5, p[2]),
velocity=(t[0] * 8, 2, t[2] * 8),
bomb_type='ice').autoretain()
elif self.hitpoints > self.hitpoints_max * 1 / 4:
Bomb(position=(p[0], p[1] - 1.5, p[2]),
velocity=(t[0] * 15, 2, t[2] * 15),
bomb_type='sticky').autoretain()
else:
Bomb(position=(p[0], p[1] - 1.5, p[2]),
velocity=(t[0] * 15, 2, t[2] * 15),
bomb_type='impact').autoretain()
def _levitate(self):
node = ba.getcollision().opposingnode
if node.exists():
p = node.getdelegate(Spaz, True)
def raise_player(player: ba.Player):
if player.is_alive():
node = player.node
try:
node.handlemessage("impulse", node.position[0],
node.position[1] + .5,
node.position[2], 0, 5, 0, 3, 10, 0,
0, 0, 5, 0)
except:
pass
if not self.frozen:
for i in range(7):
ba.timer(0.05 + i / 20, ba.Call(raise_player, p))
def on_punched(self, damage: int) -> None:
"""Called when this spaz gets punched."""
def do_damage(self, msg: Any) -> None:
if not self.node:
return None
damage = abs(msg.magnitude)
if msg.hit_type == 'explosion':
damage /= 20
else:
damage /= 5
self.hitpoints -= int(damage)
if self.hitpoints <= 0:
self.handlemessage(ba.DieMessage())
def _get_target_player_pt(self) -> tuple[
ba.Vec3 | None, ba.Vec3 | None]:
"""Returns the position and velocity of our target.
Both values will be None in the case of no target.
"""
assert self.node
botpt = ba.Vec3(self.node.position)
closest_dist: float | None = None
closest_vel: ba.Vec3 | None = None
closest: ba.Vec3 | None = None
assert self._player_pts is not None
for plpt, plvel in self._player_pts:
dist = (plpt - botpt).length()
# Ignore player-points that are significantly below the bot
# (keeps bots from following players off cliffs).
if (closest_dist is None or dist < closest_dist) and (
plpt[1] > botpt[1] - 5.0
):
closest_dist = dist
closest_vel = plvel
closest = plpt
if closest_dist is not None:
assert closest_vel is not None
assert closest is not None
return (
ba.Vec3(closest[0], closest[1], closest[2]),
ba.Vec3(closest_vel[0], closest_vel[1], closest_vel[2]),
)
return None, None
def set_player_points(self, pts: list[tuple[ba.Vec3, ba.Vec3]]) -> None:
"""Provide the spaz-bot with the locations of its enemies."""
self._player_pts = pts
def exists(self) -> bool:
return bool(self.node)
def show_damage_count(self, damage: str, position: Sequence[float],
direction: Sequence[float]) -> None:
"""Pop up a damage count at a position in space.
Category: Gameplay Functions
"""
lifespan = 1.0
app = ba.app
# FIXME: Should never vary game elements based on local config.
# (connected clients may have differing configs so they won't
# get the intended results).
do_big = app.ui.uiscale is ba.UIScale.SMALL or app.vr_mode
txtnode = ba.newnode('text',
attrs={
'text': damage,
'in_world': True,
'h_align': 'center',
'flatness': 1.0,
'shadow': 1.0 if do_big else 0.7,
'color': (1, 0.25, 0.25, 1),
'scale': 0.035 if do_big else 0.03
})
# Translate upward.
tcombine = ba.newnode('combine', owner=txtnode, attrs={'size': 3})
tcombine.connectattr('output', txtnode, 'position')
v_vals = []
pval = 0.0
vval = 0.07
count = 6
for i in range(count):
v_vals.append((float(i) / count, pval))
pval += vval
vval *= 0.5
p_start = position[0]
p_dir = direction[0]
ba.animate(tcombine, 'input0',
{i[0] * lifespan: p_start + p_dir * i[1]
for i in v_vals})
p_start = position[1]
p_dir = direction[1]
ba.animate(tcombine, 'input1',
{i[0] * lifespan: p_start + p_dir * i[1]
for i in v_vals})
p_start = position[2]
p_dir = direction[2]
ba.animate(tcombine, 'input2',
{i[0] * lifespan: p_start + p_dir * i[1]
for i in v_vals})
ba.animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0})
ba.timer(lifespan, txtnode.delete)
def _scoreboard(self) -> None:
self._backing = ba.NodeActor(
ba.newnode('image',
attrs={
'position': (self.bar_posx + self._width / 2, -100),
'scale': (self._width, self._height),
'opacity': 0.7,
'color': (0.3,
0.3,
0.3),
'vr_depth': -3,
'attach': 'topCenter',
'texture': self._backing_tex
}))
self._bar = ba.NodeActor(
ba.newnode('image',
attrs={
'opacity': 1.0,
'color': (0.5, 0.5, 0.5),
'attach': 'topCenter',
'texture': self._bar_tex
}))
self._bar_scale = ba.newnode('combine',
owner=self._bar.node,
attrs={
'size': 2,
'input0': self._bar_width,
'input1': self._bar_height
})
self._bar_scale.connectattr('output', self._bar.node, 'scale')
self._bar_position = ba.newnode(
'combine',
owner=self._bar.node,
attrs={
'size': 2,
'input0': self.bar_posx + self._bar_width / 2,
'input1': -100
})
self._bar_position.connectattr('output', self._bar.node, 'position')
self._cover = ba.NodeActor(
ba.newnode('image',
attrs={
'position': (self.bar_posx + 120, -100),
'scale':
(self._width * 1.15, self._height * 1.6),
'opacity': 1.0,
'color': (0.3,
0.3,
0.3),
'vr_depth': 2,
'attach': 'topCenter',
'texture': self._cover_tex,
'model_transparent': self._model
}))
self._score_text = ba.NodeActor(
ba.newnode('text',
attrs={
'position': (self.bar_posx + 120, -100),
'h_attach': 'center',
'v_attach': 'top',
'h_align': 'center',
'v_align': 'center',
'maxwidth': 130,
'scale': 0.9,
'text': '',
'shadow': 0.5,
'flatness': 1.0,
'color': (1, 1, 1, 0.8)
}))
def _update(self) -> None:
self._score_text.node.text = str(self.hitpoints)
self._bar_width = self.hitpoints * self._width_max / self.hitpoints_max
cur_width = self._bar_scale.input0
ba.animate(self._bar_scale, 'input0', {
0.0: cur_width,
0.1: self._bar_width
})
cur_x = self._bar_position.input0
ba.animate(self._bar_position, 'input0', {
0.0: cur_x,
0.1: self.bar_posx + self._bar_width / 2
})
if self.hitpoints > self.hitpoints_max * 3 / 4:
ba.animate_array(self.shield_deco, 'color', 3,
{0: self.shield_deco.color, 0.2: (4, 4, 4)})
elif self.hitpoints > self.hitpoints_max * 1 / 2:
ba.animate_array(self.shield_deco, 'color', 3,
{0: self.shield_deco.color, 0.2: (3, 3, 5)})
self.bot_count = 4
elif self.hitpoints > self.hitpoints_max * 1 / 4:
ba.animate_array(self.shield_deco, 'color', 3,
{0: self.shield_deco.color, 0.2: (1, 5, 1)})
self.bot_count = 5
else:
ba.animate_array(self.shield_deco, 'color', 3,
{0: self.shield_deco.color, 0.2: (5, 0.2, 0.2)})
self.bot_count = 6
def update_ai(self) -> None:
"""Should be called periodically to update the spaz' AI."""
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
if self.update_callback is not None:
if self.update_callback(self):
# Bot has been handled.
return
if not self.node:
return
pos = self.node.position
our_pos = ba.Vec3(pos[0], pos[1] - self.y_pos, pos[2])
target_pt_raw: ba.Vec3 | None
target_vel: ba.Vec3 | None
target_pt_raw, target_vel = self._get_target_player_pt()
try:
dist_raw = (target_pt_raw - our_pos).length()
target_pt = (
target_pt_raw + target_vel * dist_raw * 0.3
)
except:
return
diff = target_pt - our_pos
# self.dist = diff.length()
self.dist = diff
self.to_target = diff.normalized()
# p = spaz.node.position
# pt = self.getTargetPosition(p)
# pn = self.node.position
# d = [pt[0] - pn[0], pt[1] - pn[1], pt[2] - pn[2]]
# speed = self.getMaxSpeedByDir(d)
# self.node.velocity = (self.to_target.x, self.to_target.y, self.to_target.z)
if self.hitpoints == 0:
setattr(self.node, 'velocity',
(0, self.to_target.y, 0))
setattr(self.node, 'extra_acceleration',
(0, self.to_target.y * 80 + 70,
0))
else:
setattr(self.node, 'velocity',
(self.to_target.x * self.xz_pos,
self.to_target.y,
self.to_target.z * self.xz_pos))
setattr(self.node, 'extra_acceleration',
(self.to_target.x * self.xz_pos ,
self.to_target.y * 80 + 70,
self.to_target.z * self.xz_pos))
def on_expire(self) -> None:
super().on_expire()
# We're being torn down; release our callback(s) so there's
# no chance of them keeping activities or other things alive.
self.update_callback = None
def animate_model(self) -> None:
if not self.node:
return None
# ba.animate(self.node, 'model_scale', {
# 0: self.node.model_scale,
# 0.08: self.node.model_scale * 0.9,
# 0.15: self.node.model_scale})
ba.emitfx(position=self.node.position,
velocity=self.node.velocity,
count=int(6 + random.random() * 10),
scale=0.5,
spread=0.4,
chunk_type='metal')
def handlemessage(self, msg: Any) -> Any:
# pylint: disable=too-many-branches
assert not self.expired
if isinstance(msg, ba.HitMessage):
# Don't die on punches (that's annoying).
self.animate_model()
if self.hitpoints != 0:
self.do_damage(msg)
# self.show_damage_msg(msg)
self._update()
elif isinstance(msg, ba.DieMessage):
if self.node:
self.hitpoints = 0
self.frozen = True
self.suck_timer = False
self.drop_bomb_timer = False
self.drop_bots_timer = False
p = self.node.position
for i in range(6):
def ded_explode(count):
p_x = p[0] + random.uniform(-1, 1)
p_z = p[2] + random.uniform(-1, 1)
if count == 5:
Blast(
position=(p[0], p[1], p[2]),
blast_type='tnt',
blast_radius=5.0).autoretain()
else:
Blast(
position=(p_x, p[1], p_z),
blast_radius=2.0).autoretain()
ba.timer(0 + i, ba.Call(ded_explode, i))
ba.timer(5, self.node.delete)
ba.timer(0.1, self.suck.delete)
ba.timer(0.1, self.suck_anim.delete)
elif isinstance(msg, ba.OutOfBoundsMessage):
activity = _ba.get_foreground_host_activity()
try:
point = activity.map.get_flag_position(None)
boss_spawn_pos = (point[0], point[1] + 1.5, point[2])
assert self.node
self.node.position = boss_spawn_pos
except:
self.handlemessage(ba.DieMessage())
elif isinstance(msg, ba.FreezeMessage):
if not self.frozen:
self.frozen = True
self.y_pos = -1.5
self.xz_pos = 0.01
self.node.reflection_scale = [2]
def unfrozen():
self.frozen = False
if self.bot_dur_froze:
ba.timer(0.1, ba.Call(self._drop_bots))
self.bot_dur_froze = False
self.y_pos = 3
self.xz_pos = 1
self.node.reflection_scale = [0.25]
ba.timer(5.0, unfrozen)
else:
super().handlemessage(msg)
class UFOSet:
"""A container/controller for one or more ba.SpazBots.
category: Bot Classes
"""
def __init__(self) -> None:
"""Create a bot-set."""
# We spread our bots out over a few lists so we can update
# them in a staggered fashion.
self._ufo_bot_list_count = 5
self._ufo_bot_add_list = 0
self._ufo_bot_update_list = 0
self._ufo_bot_lists: list[list[UFO]] = [
[] for _ in range(self._ufo_bot_list_count)
]
self._ufo_spawn_sound = ba.getsound('spawn')
self._ufo_spawning_count = 0
self._ufo_bot_update_timer: ba.Timer | None = None
self.start_moving()
def _update(self) -> None:
# Update one of our bot lists each time through.
# First off, remove no-longer-existing bots from the list.
try:
bot_list = self._ufo_bot_lists[self._ufo_bot_update_list] = [
b for b in self._ufo_bot_lists[self._ufo_bot_update_list] if b
]
except Exception:
bot_list = []
ba.print_exception(
'Error updating bot list: '
+ str(self._ufo_bot_lists[self._ufo_bot_update_list])
)
self._bot_update_list = (
self._ufo_bot_update_list + 1
) % self._ufo_bot_list_count
# Update our list of player points for the bots to use.
player_pts = []
for player in ba.getactivity().players:
assert isinstance(player, ba.Player)
try:
# TODO: could use abstracted player.position here so we
# don't have to assume their actor type, but we have no
# abstracted velocity as of yet.
if player.is_alive():
assert isinstance(player.actor, UFO)
assert player.actor.node
player_pts.append(
(
ba.Vec3(player.actor.node.position),
ba.Vec3(player.actor.node.velocity),
)
)
except Exception:
ba.print_exception('Error on bot-set _update.')
for bot in bot_list:
bot.set_player_points(player_pts)
bot.update_ai()
def start_moving(self) -> None:
"""Start processing bot AI updates so they start doing their thing."""
self._ufo_bot_update_timer = ba.Timer(
0.05, ba.WeakCall(self._update), repeat=True
)
def spawn_bot(
self,
bot_type: type[UFO],
pos: Sequence[float],
spawn_time: float = 3.0,
on_spawn_call: Callable[[UFO], Any] | None = None,
) -> None:
"""Spawn a bot from this set."""
from bastd.actor import spawner
spawner.Spawner(
pt=pos,
spawn_time=spawn_time,
send_spawn_message=False,
spawn_callback=ba.Call(
self._spawn_bot, bot_type, pos, on_spawn_call
),
)
self._ufo_spawning_count += 1
def _spawn_bot(
self,
bot_type: type[UFO],
pos: Sequence[float],
on_spawn_call: Callable[[UFO], Any] | None,
) -> None:
spaz = bot_type()
ba.playsound(self._ufo_spawn_sound, position=pos)
assert spaz.node
spaz.node.handlemessage('flash')
spaz.node.is_area_of_interest = False
spaz.handlemessage(ba.StandMessage(pos, random.uniform(0, 360)))
self.add_bot(spaz)
self._ufo_spawning_count -= 1
if on_spawn_call is not None:
on_spawn_call(spaz)
def add_bot(self, bot: UFO) -> None:
"""Add a ba.SpazBot instance to the set."""
self._ufo_bot_lists[self._ufo_bot_add_list].append(bot)
self._ufo_bot_add_list = (
self._ufo_bot_add_list + 1) % self._ufo_bot_list_count
def have_living_bots(self) -> bool:
"""Return whether any bots in the set are alive or spawning."""
return self._ufo_spawning_count > 0 or any(
any(b.is_alive() for b in l) for l in self._ufo_bot_lists
)
def get_living_bots(self) -> list[UFO]:
"""Get the living bots in the set."""
bots: list[UFO] = []
for botlist in self._ufo_bot_lists:
for bot in botlist:
if bot.is_alive():
bots.append(bot)
return bots
def clear(self) -> None:
"""Immediately clear out any bots in the set."""
# Don't do this if the activity is shutting down or dead.
activity = ba.getactivity(doraise=False)
if activity is None or activity.expired:
return
for i, bot_list in enumerate(self._ufo_bot_lists):
for bot in bot_list:
bot.handlemessage(ba.DieMessage(immediate=True))
self._ufo_bot_lists[i] = []
class Player(ba.Player['Team']):
"""Our player type for this game."""
class Team(ba.Team[Player]):
"""Our team type for this game."""
# ba_meta export game
class UFOightGame(ba.TeamGameActivity[Player, Team]):
"""
A co-op game where you try to defeat UFO Boss
as fast as possible
"""
name = 'UFO Fight'
description = 'REal Boss Fight?'
scoreconfig = ba.ScoreConfig(
label='Time', scoretype=ba.ScoreType.MILLISECONDS, lower_is_better=True
)
default_music = ba.MusicType.TO_THE_DEATH
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.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 ['Football Stadium']
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
# We currently support Co-Op only.
return issubclass(sessiontype, ba.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 = ba.getsound('score')
self._won = False
self._timer: OnScreenTimer | None = None
self._bots = UFOSet()
self._preset = str(settings['preset'])
self._credit = ba.newnode('text',
attrs={
'v_attach': 'bottom',
'h_align': 'center',
'color': (0.4, 0.4, 0.4),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, 20),
'scale': 0.7,
'text': 'By Cross Joy'
})
def on_transition_in(self) -> None:
super().on_transition_in()
gnode = ba.getactivity().globalsnode
gnode.tint = (0.42, 0.55, 0.66)
# Called when our game actually begins.
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_powerup_drops()
# In pro mode there's no powerups.
# Make our on-screen timer and start it roughly when our bots appear.
self._timer = OnScreenTimer()
ba.timer(4.0, self._timer.start)
def checker():
if not self._won:
self.timer = ba.Timer(0.1, self._check_if_won, repeat=True)
ba.timer(10, checker)
activity = _ba.get_foreground_host_activity()
point = activity.map.get_flag_position(None)
boss_spawn_pos = (point[0], point[1] + 1.5, point[2])
# Spawn some baddies.
ba.timer(
1.0,
lambda: self._bots.spawn_bot(
UFO, pos=boss_spawn_pos, spawn_time=3.0
),
)
# Called for each spawning player.
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.timer = False
self._won = True
self.end_game()
# Called for miscellaneous messages.
def handlemessage(self, msg: Any) -> Any:
# A player has died.
if isinstance(msg, ba.PlayerDiedMessage):
player = msg.getplayer(Player)
self.stats.player_was_killed(player)
ba.timer(0.1, self._checkroundover)
# A spaz-bot has died.
elif isinstance(msg, UFODiedMessage):
# Unfortunately the ufo 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.
ba.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 _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()
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 = ba.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((ba.time() - self._timer.starttime) * 1000.0)
ba.cameraflash()
ba.playsound(self._winsound)
for team in self.teams:
for player in team.players:
if player.actor:
player.actor.handlemessage(ba.CelebrateMessage())
results.set_team_score(team, elapsed_time_ms)
# Ends the activity.
self.end(results)
# ba_meta export plugin
class MyUFOFightLevel(ba.Plugin):
def on_app_running(self) -> None:
ba.app.add_coop_practice_level(
ba.Level(
name='The UFO Fight',
displayname='${GAME}',
gametype=UFOightGame,
settings={'preset': 'regular'},
preview_texture_name='footballStadiumPreview',
)
)

View file

@ -0,0 +1,222 @@
#Made by your friend: @[Just] Freak#4999
# ba_meta require api 7
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
if TYPE_CHECKING:
from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional
class Player(ba.Player['Team']):
"""Our player type for this game."""
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.score = 0
# ba_meta export game
class BoxingGame(ba.TeamGameActivity[Player, Team]):
"""A game of yeeting people out of map"""
name = 'Yeeting Party!'
description = 'Yeet your enemies out of the map'
# 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 ['Bridgit', 'Rampage', 'Monkey Face']
def __init__(self, settings: dict):
super().__init__(settings)
self._scoreboard = Scoreboard()
self._score_to_win: Optional[int] = None
self._dingsound = ba.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 = (ba.MusicType.EPIC if self._epic_mode else
ba.MusicType.TO_THE_DEATH)
def get_instance_description(self) -> Union[str, Sequence]:
return 'Yeet ${ARG1} enemies out of the map!', self._score_to_win
def get_instance_description_short(self) -> Union[str, Sequence]:
return 'yeet ${ARG1} enemies', self._score_to_win
def on_team_join(self, team: Team) -> None:
if self.has_begun():
self._update_scoreboard()
def getRandomPowerupPoint(self) -> None:
myMap = self.map.getname()
if myMap == 'Doom Shroom':
while True:
x = random.uniform(-1.0,1.0)
y = random.uniform(-1.0,1.0)
if x*x+y*y < 1.0: break
return ((8.0*x,2.5,-3.5+5.0*y))
elif myMap == 'Rampage':
x = random.uniform(-6.0,7.0)
y = random.uniform(-6.0,-2.5)
return ((x, 5.2, y))
else:
x = random.uniform(-5.0,5.0)
y = random.uniform(-6.0,0.0)
return ((x, 8.0, y))
def on_begin(self) -> None:
super().on_begin()
ba.screenmessage("start Yeeting",color = (0.2,1,1))
self.setup_standard_time_limit(self._time_limit)
# 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)))
self._update_scoreboard()
def spawn_player(self, player: Player) -> ba.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_jump=True,
enable_bomb=False,
enable_pickup=True)
return spaz
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.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, ba.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:
ba.playsound(self._dingsound)
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
ba.playsound(self._dingsound)
# 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):
ba.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 = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.score)
self.end(results=results)