mirror of
https://github.com/bombsquad-community/plugin-manager.git
synced 2025-10-08 14:54:36 +00:00
Added many minigames
This commit is contained in:
parent
2e8ee6387e
commit
4af785998d
11 changed files with 5296 additions and 2 deletions
|
|
@ -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
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
194
plugins/minigames/arms_race.py
Normal file
194
plugins/minigames/arms_race.py
Normal 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)
|
||||
642
plugins/minigames/collector.py
Normal file
642
plugins/minigames/collector.py
Normal 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)
|
||||
762
plugins/minigames/dodge_the_ball.py
Normal file
762
plugins/minigames/dodge_the_ball.py
Normal 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)
|
||||
|
||||
|
||||
333
plugins/minigames/invisible_one.py
Normal file
333
plugins/minigames/invisible_one.py
Normal 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)
|
||||
263
plugins/minigames/last_punch_stand.py
Normal file
263
plugins/minigames/last_punch_stand.py
Normal 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
642
plugins/minigames/quake.py
Normal 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])
|
||||
783
plugins/minigames/sleep_race.py
Normal file
783
plugins/minigames/sleep_race.py
Normal 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
314
plugins/minigames/snake.py
Normal 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)
|
||||
994
plugins/minigames/ufo_fight.py
Normal file
994
plugins/minigames/ufo_fight.py
Normal 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',
|
||||
)
|
||||
)
|
||||
|
||||
222
plugins/minigames/yeeting_party.py
Normal file
222
plugins/minigames/yeeting_party.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue