bombsquad-plugin-manager/plugins/minigames/ultimate_last_stand.py

624 lines
23 KiB
Python
Raw Normal View History

2024-01-17 23:09:18 +03:00
# Porting to api 8 made easier by baport.(https://github.com/bombsquad-community/baport)
"""Ultimate Last Stand V2:
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://discord.gg/JyBY6haARJ
# ----------------------------------------------------------------------------
# V2 What's new?
# - The "Player can't fight each other" system is removed,
# players exploiting the features and, I know ideas how to fix it especially
# the freeze handlemessage
# - Added new bot: Ice Bot
# - The bot spawn location will be more randomize rather than based on players
# position, I don't wanna players stay at the corner of the map.
# - Some codes clean up.
# ----------------------------------------------------------------------------
2024-01-17 23:09:18 +03:00
# ba_meta require api 8
from __future__ import annotations
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING
2024-01-17 23:09:18 +03:00
import babase
import bauiv1 as bui
import bascenev1 as bs
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.bomb import TNTSpawner
from bascenev1lib.actor.onscreentimer import OnScreenTimer
from bascenev1lib.actor.scoreboard import Scoreboard
from bascenev1lib.actor.spazfactory import SpazFactory
from bascenev1lib.actor.spazbot import (SpazBot, SpazBotSet, BomberBot,
2024-01-18 13:41:11 +00:00
BomberBotPro, BomberBotProShielded,
BrawlerBot, BrawlerBotPro,
BrawlerBotProShielded, TriggerBot,
TriggerBotPro, TriggerBotProShielded,
ChargerBot, StickyBot, ExplodeyBot)
if TYPE_CHECKING:
from typing import Any, Sequence
2024-01-17 23:09:18 +03:00
from bascenev1lib.actor.spazbot import SpazBot
class IceBot(SpazBot):
"""A slow moving bot with ice bombs.
category: Bot Classes
"""
character = 'Pascal'
punchiness = 0.9
throwiness = 1
charge_speed_min = 1
charge_speed_max = 1
throw_dist_min = 5.0
throw_dist_max = 20
run = True
charge_dist_min = 10.0
charge_dist_max = 11.0
default_bomb_type = 'ice'
default_bomb_count = 1
points_mult = 3
2024-01-17 23:09:18 +03:00
class Icon(bs.Actor):
"""Creates in in-game icon on screen."""
def __init__(self,
player: Player,
position: tuple[float, float],
scale: float,
show_lives: bool = True,
show_death: bool = True,
name_scale: float = 1.0,
name_maxwidth: float = 115.0,
flatness: float = 1.0,
shadow: float = 1.0):
super().__init__()
self._player = player
self._show_lives = show_lives
self._show_death = show_death
self._name_scale = name_scale
2024-01-17 23:09:18 +03:00
self._outline_tex = bs.gettexture('characterIconMask')
icon = player.get_icon()
2024-01-17 23:09:18 +03:00
self.node = bs.newnode('image',
delegate=self,
attrs={
'texture': icon['texture'],
'tint_texture': icon['tint_texture'],
'tint_color': icon['tint_color'],
'vr_depth': 400,
'tint2_color': icon['tint2_color'],
'mask_texture': self._outline_tex,
'opacity': 1.0,
'absolute_scale': True,
'attach': 'bottomCenter'
})
2024-01-17 23:09:18 +03:00
self._name_text = bs.newnode(
'text',
owner=self.node,
attrs={
2024-01-17 23:09:18 +03:00
'text': babase.Lstr(value=player.getname()),
'color': babase.safecolor(player.team.color),
'h_align': 'center',
'v_align': 'center',
'vr_depth': 410,
'maxwidth': name_maxwidth,
'shadow': shadow,
'flatness': flatness,
'h_attach': 'center',
'v_attach': 'bottom'
})
if self._show_lives:
2024-01-17 23:09:18 +03:00
self._lives_text = bs.newnode('text',
owner=self.node,
attrs={
'text': 'x0',
'color': (1, 1, 0.5),
'h_align': 'left',
'vr_depth': 430,
'shadow': 1.0,
'flatness': 1.0,
'h_attach': 'center',
'v_attach': 'bottom'
})
self.set_position_and_scale(position, scale)
def set_position_and_scale(self, position: tuple[float, float],
scale: float) -> None:
"""(Re)position the icon."""
assert self.node
self.node.position = position
self.node.scale = [70.0 * scale]
self._name_text.position = (position[0], position[1] + scale * 52.0)
self._name_text.scale = 1.0 * scale * self._name_scale
if self._show_lives:
self._lives_text.position = (position[0] + scale * 10.0,
position[1] - scale * 43.0)
self._lives_text.scale = 1.0 * scale
def update_for_lives(self) -> None:
"""Update for the target player's current lives."""
if self._player:
lives = self._player.lives
else:
lives = 0
if self._show_lives:
if lives > 0:
self._lives_text.text = 'x' + str(lives - 1)
else:
self._lives_text.text = ''
if lives == 0:
self._name_text.opacity = 0.2
assert self.node
self.node.color = (0.7, 0.3, 0.3)
self.node.opacity = 0.2
def handle_player_spawned(self) -> None:
"""Our player spawned; hooray!"""
if not self.node:
return
self.node.opacity = 1.0
self.update_for_lives()
def handle_player_died(self) -> None:
"""Well poo; our player died."""
if not self.node:
return
if self._show_death:
2024-01-17 23:09:18 +03:00
bs.animate(
self.node, 'opacity', {
0.00: 1.0,
0.05: 0.0,
0.10: 1.0,
0.15: 0.0,
0.20: 1.0,
0.25: 0.0,
0.30: 1.0,
0.35: 0.0,
0.40: 1.0,
0.45: 0.0,
0.50: 1.0,
0.55: 0.2
})
lives = self._player.lives
if lives == 0:
2024-01-17 23:09:18 +03:00
bs.timer(0.6, self.update_for_lives)
def handlemessage(self, msg: Any) -> Any:
2024-01-17 23:09:18 +03:00
if isinstance(msg, bs.DieMessage):
self.node.delete()
return None
return super().handlemessage(msg)
@dataclass
class SpawnInfo:
"""Spawning info for a particular bot type."""
spawnrate: float
increase: float
dincrease: float
2024-01-17 23:09:18 +03:00
class Player(bs.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
super().__init__()
self.death_time: float | None = None
self.lives = 0
self.icons: list[Icon] = []
2024-01-17 23:09:18 +03:00
class Team(bs.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.survival_seconds: int | None = None
self.spawn_order: list[Player] = []
2024-01-17 23:09:18 +03:00
# ba_meta export bascenev1.GameActivity
class UltimateLastStand(bs.TeamGameActivity[Player, Team]):
"""Minigame involving dodging falling bombs."""
name = 'Ultimate Last Stand'
description = 'Only the strongest will stand at the end.'
2024-01-17 23:09:18 +03:00
scoreconfig = bs.ScoreConfig(label='Survived',
scoretype=bs.ScoreType.SECONDS,
none_is_winner=True)
# Print messages when players die (since its meaningful in this game).
announce_player_deaths = True
# Don't allow joining after we start
# (would enable leave/rejoin tomfoolery).
allow_mid_activity_joins = False
@classmethod
def get_available_settings(
2022-10-22 10:31:05 +00:00
cls,
2024-01-17 23:09:18 +03:00
sessiontype: type[bs.Session]) -> list[babase.Setting]:
settings = [
2024-01-17 23:09:18 +03:00
bs.IntSetting(
'Lives Per Player',
default=1,
min_value=1,
max_value=10,
increment=1,
),
2024-01-17 23:09:18 +03:00
bs.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
2024-01-17 23:09:18 +03:00
bs.BoolSetting('Epic Mode', default=False),
]
2024-01-17 23:09:18 +03:00
if issubclass(sessiontype, bs.DualTeamSession):
settings.append(
2024-01-17 23:09:18 +03:00
bs.BoolSetting('Balance Total Lives', default=False))
return settings
# We're currently hard-coded for one map.
@classmethod
2024-01-17 23:09:18 +03:00
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
return ['Rampage']
# We support teams, free-for-all, and co-op sessions.
@classmethod
2024-01-17 23:09:18 +03:00
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return (issubclass(sessiontype, bs.DualTeamSession)
or issubclass(sessiontype, bs.FreeForAllSession))
def __init__(self, settings: dict):
super().__init__(settings)
self._scoreboard = Scoreboard()
self._start_time: float | None = None
2024-01-17 23:09:18 +03:00
self._vs_text: bs.Actor | None = None
self._round_end_timer: bs.Timer | None = None
self._lives_per_player = int(settings['Lives Per Player'])
self._balance_total_lives = bool(
settings.get('Balance Total Lives', False))
self._epic_mode = settings.get('Epic Mode', True)
self._last_player_death_time: float | None = None
self._timer: OnScreenTimer | None = None
self._tntspawner: TNTSpawner | None = None
2024-01-17 23:09:18 +03:00
self._new_wave_sound = bs.getsound('scoreHit01')
self._bots = SpazBotSet()
self._tntspawnpos = (0, 5.5, -6)
self.spazList = []
# Base class overrides:
self.slow_motion = self._epic_mode
2024-01-17 23:09:18 +03:00
self.default_music = (bs.MusicType.EPIC
if self._epic_mode else bs.MusicType.SURVIVAL)
2024-01-17 23:09:18 +03:00
self.node = bs.newnode('text',
attrs={
'v_attach': 'bottom',
'h_align': 'center',
'color': (0.83, 0.69, 0.21),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, 75),
'scale': 0.7,
'text': 'By Cross Joy'
})
# For each bot type: [spawnrate, increase, d_increase]
self._bot_spawn_types = {
BomberBot: SpawnInfo(1.00, 0.00, 0.000),
BomberBotPro: SpawnInfo(0.00, 0.05, 0.001),
BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
BrawlerBot: SpawnInfo(1.00, 0.00, 0.000),
BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001),
BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
TriggerBot: SpawnInfo(0.30, 0.00, 0.000),
TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001),
TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
ChargerBot: SpawnInfo(0.30, 0.05, 0.000),
StickyBot: SpawnInfo(0.10, 0.03, 0.001),
IceBot: SpawnInfo(0.10, 0.03, 0.001),
ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002)
} # yapf: disable
# Some base class overrides:
2024-01-17 23:09:18 +03:00
self.default_music = (bs.MusicType.EPIC
if self._epic_mode else bs.MusicType.SURVIVAL)
if self._epic_mode:
self.slow_motion = True
def get_instance_description(self) -> str | Sequence:
return 'Only the strongest team will stand at the end.' if isinstance(
self.session,
2024-01-17 23:09:18 +03:00
bs.DualTeamSession) else 'Only the strongest will stand at the end.'
def get_instance_description_short(self) -> str | Sequence:
return 'Only the strongest team will stand at the end.' if isinstance(
self.session,
2024-01-17 23:09:18 +03:00
bs.DualTeamSession) else 'Only the strongest will stand at the end.'
def on_transition_in(self) -> None:
super().on_transition_in()
2024-01-22 12:06:37 +03:00
bs.timer(1.3, self._new_wave_sound.play)
def on_player_join(self, player: Player) -> None:
player.lives = self._lives_per_player
# Don't waste time doing this until begin.
player.icons = [Icon(player, position=(0, 50), scale=0.8)]
if player.lives > 0:
self.spawn_player(player)
if self.has_begun():
self._update_icons()
def on_begin(self) -> None:
super().on_begin()
2024-01-17 23:09:18 +03:00
bs.animate_array(node=self.node, attr='color', size=3, keys={
0.0: (0.5, 0.5, 0.5),
0.8: (0.83, 0.69, 0.21),
1.6: (0.5, 0.5, 0.5)
}, loop=True)
2024-01-17 23:09:18 +03:00
bs.timer(0.001, bs.WeakCall(self._start_bot_updates))
self._tntspawner = TNTSpawner(position=self._tntspawnpos,
respawn_time=10.0)
self._timer = OnScreenTimer()
self._timer.start()
self.setup_standard_powerup_drops()
# Check for immediate end (if we've only got 1 player, etc).
2024-01-17 23:09:18 +03:00
self._start_time = bs.time()
# If balance-team-lives is on, add lives to the smaller team until
# total lives match.
2024-01-17 23:09:18 +03:00
if (isinstance(self.session, bs.DualTeamSession)
and self._balance_total_lives and self.teams[0].players
2022-10-22 10:31:05 +00:00
and self.teams[1].players):
if self._get_total_team_lives(
self.teams[0]) < self._get_total_team_lives(self.teams[1]):
lesser_team = self.teams[0]
greater_team = self.teams[1]
else:
lesser_team = self.teams[1]
greater_team = self.teams[0]
add_index = 0
while (self._get_total_team_lives(lesser_team) <
self._get_total_team_lives(greater_team)):
lesser_team.players[add_index].lives += 1
add_index = (add_index + 1) % len(lesser_team.players)
2024-01-17 23:09:18 +03:00
bs.timer(1.0, self._update, repeat=True)
self._update_icons()
# We could check game-over conditions at explicit trigger points,
# but lets just do the simple thing and poll it.
def _update_icons(self) -> None:
# pylint: disable=too-many-branches
# In free-for-all mode, everyone is just lined up along the bottom.
2024-01-17 23:09:18 +03:00
if isinstance(self.session, bs.FreeForAllSession):
count = len(self.teams)
x_offs = 85
xval = x_offs * (count - 1) * -0.5
for team in self.teams:
if len(team.players) == 1:
player = team.players[0]
for icon in player.icons:
icon.set_position_and_scale((xval, 30), 0.7)
icon.update_for_lives()
xval += x_offs
# In teams mode we split up teams.
else:
for team in self.teams:
if team.id == 0:
xval = -50
x_offs = -85
else:
xval = 50
x_offs = 85
for player in team.players:
for icon in player.icons:
icon.set_position_and_scale((xval, 30), 0.7)
icon.update_for_lives()
xval += x_offs
def on_player_leave(self, player: Player) -> None:
# Augment default behavior.
super().on_player_leave(player)
player.icons = []
# Update icons in a moment since our team will be gone from the
# list then.
2024-01-17 23:09:18 +03:00
bs.timer(0, self._update_icons)
# If the player to leave was the last in spawn order and had
# their final turn currently in-progress, mark the survival time
# for their team.
if self._get_total_team_lives(player.team) == 0:
assert self._start_time is not None
2024-01-17 23:09:18 +03:00
player.team.survival_seconds = int(bs.time() - self._start_time)
# A departing player may trigger game-over.
# overriding the default character spawning..
2024-01-17 23:09:18 +03:00
def spawn_player(self, player: Player) -> bs.Actor:
actor = self.spawn_player_spaz(player)
2024-01-17 23:09:18 +03:00
bs.timer(0.3, babase.Call(self._print_lives, player))
# If we have any icons, update their state.
for icon in player.icons:
icon.handle_player_spawned()
return actor
def _print_lives(self, player: Player) -> None:
2024-01-17 23:09:18 +03:00
from bascenev1lib.actor import popuptext
# We get called in a timer so it's possible our player has left/etc.
if not player or not player.is_alive() or not player.node:
return
popuptext.PopupText('x' + str(player.lives - 1),
color=(1, 1, 0, 1),
offset=(0, -0.8, 0),
random_offset=0.0,
scale=1.8,
position=player.node.position).autoretain()
def _get_total_team_lives(self, team: Team) -> int:
return sum(player.lives for player in team.players)
def _start_bot_updates(self) -> None:
self._bot_update_interval = 3.3 - 0.3 * (len(self.players))
self._update_bots()
self._update_bots()
if len(self.players) > 2:
self._update_bots()
if len(self.players) > 3:
self._update_bots()
2024-01-17 23:09:18 +03:00
self._bot_update_timer = bs.Timer(self._bot_update_interval,
bs.WeakCall(self._update_bots))
def _update_bots(self) -> None:
assert self._bot_update_interval is not None
self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98)
2024-01-17 23:09:18 +03:00
self._bot_update_timer = bs.Timer(self._bot_update_interval,
bs.WeakCall(self._update_bots))
botspawnpts: list[Sequence[float]] = [[-5.0, 5.5, -4.14],
[0.0, 5.5, -4.14],
[5.0, 5.5, -4.14]]
for player in self.players:
try:
if player.is_alive():
assert isinstance(player.actor, PlayerSpaz)
assert player.actor.node
except Exception:
2024-01-17 23:09:18 +03:00
babase.print_exception('Error updating bots.')
spawnpt = random.choice(
[botspawnpts[0], botspawnpts[1], botspawnpts[2]])
spawnpt = (spawnpt[0] + 3.0 * (random.random() - 0.5), spawnpt[1],
2.0 * (random.random() - 0.5) + spawnpt[2])
# Normalize our bot type total and find a random number within that.
total = 0.0
for spawninfo in self._bot_spawn_types.values():
total += spawninfo.spawnrate
randval = random.random() * total
# Now go back through and see where this value falls.
total = 0
bottype: type[SpazBot] | None = None
for spawntype, spawninfo in self._bot_spawn_types.items():
total += spawninfo.spawnrate
if randval <= total:
bottype = spawntype
break
spawn_time = 1.0
assert bottype is not None
self._bots.spawn_bot(bottype, pos=spawnpt, spawn_time=spawn_time)
# After every spawn we adjust our ratios slightly to get more
# difficult.
for spawninfo in self._bot_spawn_types.values():
spawninfo.spawnrate += spawninfo.increase
spawninfo.increase += spawninfo.dincrease
# Various high-level game events come through this method.
def handlemessage(self, msg: Any) -> Any:
2024-01-17 23:09:18 +03:00
if isinstance(msg, bs.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
2024-01-17 23:09:18 +03:00
curtime = bs.time()
# Record the player's moment of death.
# assert isinstance(msg.spaz.player
msg.getplayer(Player).death_time = curtime
player: Player = msg.getplayer(Player)
player.lives -= 1
if player.lives < 0:
player.lives = 0
# If we have any icons, update their state.
for icon in player.icons:
icon.handle_player_died()
# Play big death sound on our last death
# or for every one in solo mode.
if player.lives == 0:
2024-01-17 23:09:18 +03:00
SpazFactory.get().single_player_death_sound.play()
# If we hit zero lives, we're dead (and our team might be too).
if player.lives == 0:
# If the whole team is now dead, mark their survival time.
if self._get_total_team_lives(player.team) == 0:
assert self._start_time is not None
2024-01-17 23:09:18 +03:00
player.team.survival_seconds = int(bs.time() -
self._start_time)
else:
# Otherwise, in regular mode, respawn.
self.respawn_player(player)
def _get_living_teams(self) -> list[Team]:
return [
team for team in self.teams
if len(team.players) > 0 and any(player.lives > 0
for player in team.players)
]
def _update(self) -> None:
# If we're down to 1 or fewer living teams, start a timer to end
# the game (allows the dust to settle and draws to occur if deaths
# are close enough).
if len(self._get_living_teams()) < 2:
2024-01-17 23:09:18 +03:00
self._round_end_timer = bs.Timer(0.5, self.end_game)
def end_game(self) -> None:
# Stop updating our time text, and set the final time to match
# exactly when our last guy died.
self._timer.stop(endtime=self._last_player_death_time)
# Ok now calc game results: set a score for each team and then tell
# the game to end.
2024-01-17 23:09:18 +03:00
results = bs.GameResults()
# Remember that 'free-for-all' mode is simply a special form
# of 'teams' mode where each player gets their own team, so we can
# just always deal in teams and have all cases covered.
for team in self.teams:
# Submit the score value in milliseconds.
results.set_team_score(team, team.survival_seconds)
self.end(results=results)