mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2026-08-15 13:04:50 +00:00
303 lines
10 KiB
Python
303 lines
10 KiB
Python
# Released under the MIT License. See LICENSE for details.
|
|
#
|
|
"""Provides an easter egg hunt 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
|
|
|
|
import ba
|
|
from bastd.actor.bomb import Bomb
|
|
from bastd.actor.playerspaz import PlayerSpaz
|
|
from bastd.actor.spazbot import SpazBotSet, BouncyBot, SpazBotDiedMessage
|
|
from bastd.actor.onscreencountdown import OnScreenCountdown
|
|
from bastd.actor.scoreboard import Scoreboard
|
|
from bastd.actor.respawnicon import RespawnIcon
|
|
from bastd.gameutils import SharedObjects
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Any
|
|
|
|
|
|
class Player(ba.Player['Team']):
|
|
"""Our player type for this game."""
|
|
|
|
def __init__(self) -> None:
|
|
self.respawn_timer: ba.Timer | None = None
|
|
self.respawn_icon: RespawnIcon | None = None
|
|
|
|
|
|
class Team(ba.Team[Player]):
|
|
"""Our team type for this game."""
|
|
|
|
def __init__(self) -> None:
|
|
self.score = 0
|
|
|
|
|
|
# ba_meta export game
|
|
class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]):
|
|
"""A game where score is based on collecting eggs."""
|
|
|
|
name = 'Easter Egg Hunt'
|
|
description = 'Gather eggs!'
|
|
available_settings = [
|
|
ba.BoolSetting('Pro Mode', default=False),
|
|
ba.BoolSetting('Epic Mode', default=False),
|
|
]
|
|
scoreconfig = ba.ScoreConfig(label='Score', scoretype=ba.ScoreType.POINTS)
|
|
|
|
# We're currently hard-coded for one map.
|
|
@classmethod
|
|
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
|
|
return ['Tower D']
|
|
|
|
# We support teams, free-for-all, and co-op sessions.
|
|
@classmethod
|
|
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
|
|
return (
|
|
issubclass(sessiontype, ba.CoopSession)
|
|
or issubclass(sessiontype, ba.DualTeamSession)
|
|
or issubclass(sessiontype, ba.FreeForAllSession)
|
|
)
|
|
|
|
def __init__(self, settings: dict):
|
|
super().__init__(settings)
|
|
shared = SharedObjects.get()
|
|
self._last_player_death_time = None
|
|
self._scoreboard = Scoreboard()
|
|
self.egg_model = ba.getmodel('egg')
|
|
self.egg_tex_1 = ba.gettexture('eggTex1')
|
|
self.egg_tex_2 = ba.gettexture('eggTex2')
|
|
self.egg_tex_3 = ba.gettexture('eggTex3')
|
|
self._collect_sound = ba.getsound('powerup01')
|
|
self._pro_mode = settings.get('Pro Mode', False)
|
|
self._epic_mode = settings.get('Epic Mode', False)
|
|
self._max_eggs = 1.0
|
|
self.egg_material = ba.Material()
|
|
self.egg_material.add_actions(
|
|
conditions=('they_have_material', shared.player_material),
|
|
actions=(('call', 'at_connect', self._on_egg_player_collide),),
|
|
)
|
|
self._eggs: list[Egg] = []
|
|
self._update_timer: ba.Timer | None = None
|
|
self._countdown: OnScreenCountdown | None = None
|
|
self._bots: SpazBotSet | None = None
|
|
|
|
# Base class overrides
|
|
self.slow_motion = self._epic_mode
|
|
self.default_music = (
|
|
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FORWARD_MARCH
|
|
)
|
|
|
|
def on_team_join(self, team: Team) -> None:
|
|
if self.has_begun():
|
|
self._update_scoreboard()
|
|
|
|
# Called when our game actually starts.
|
|
def on_begin(self) -> None:
|
|
from bastd.maps import TowerD
|
|
|
|
# There's a player-wall on the tower-d level to prevent
|
|
# players from getting up on the stairs.. we wanna kill that.
|
|
gamemap = self.map
|
|
assert isinstance(gamemap, TowerD)
|
|
gamemap.player_wall.delete()
|
|
super().on_begin()
|
|
self._update_scoreboard()
|
|
self._update_timer = ba.Timer(0.25, self._update, repeat=True)
|
|
self._countdown = OnScreenCountdown(60, endcall=self.end_game)
|
|
ba.timer(4.0, self._countdown.start)
|
|
self._bots = SpazBotSet()
|
|
|
|
# Spawn evil bunny in co-op only.
|
|
if isinstance(self.session, ba.CoopSession) and self._pro_mode:
|
|
self._spawn_evil_bunny()
|
|
|
|
# Overriding the default character spawning.
|
|
def spawn_player(self, player: Player) -> ba.Actor:
|
|
spaz = self.spawn_player_spaz(player)
|
|
spaz.connect_controls_to_player()
|
|
return spaz
|
|
|
|
def _spawn_evil_bunny(self) -> None:
|
|
assert self._bots is not None
|
|
self._bots.spawn_bot(BouncyBot, pos=(6, 4, -7.8), spawn_time=10.0)
|
|
|
|
def _on_egg_player_collide(self) -> None:
|
|
if self.has_ended():
|
|
return
|
|
collision = ba.getcollision()
|
|
|
|
# Be defensive here; we could be hitting the corpse of a player
|
|
# who just left/etc.
|
|
try:
|
|
egg = collision.sourcenode.getdelegate(Egg, True)
|
|
player = collision.opposingnode.getdelegate(
|
|
PlayerSpaz, True
|
|
).getplayer(Player, True)
|
|
except ba.NotFoundError:
|
|
return
|
|
|
|
player.team.score += 1
|
|
|
|
# Displays a +1 (and adds to individual player score in
|
|
# teams mode).
|
|
self.stats.player_scored(player, 1, screenmessage=False)
|
|
if self._max_eggs < 5:
|
|
self._max_eggs += 1.0
|
|
elif self._max_eggs < 10:
|
|
self._max_eggs += 0.5
|
|
elif self._max_eggs < 30:
|
|
self._max_eggs += 0.3
|
|
self._update_scoreboard()
|
|
ba.playsound(self._collect_sound, 0.5, position=egg.node.position)
|
|
|
|
# Create a flash.
|
|
light = ba.newnode(
|
|
'light',
|
|
attrs={
|
|
'position': egg.node.position,
|
|
'height_attenuated': False,
|
|
'radius': 0.1,
|
|
'color': (1, 1, 0),
|
|
},
|
|
)
|
|
ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.2: 0}, loop=False)
|
|
ba.timer(0.200, light.delete)
|
|
egg.handlemessage(ba.DieMessage())
|
|
|
|
def _update(self) -> None:
|
|
# Misc. periodic updating.
|
|
xpos = random.uniform(-7.1, 6.0)
|
|
ypos = random.uniform(3.5, 3.5)
|
|
zpos = random.uniform(-8.2, 3.7)
|
|
|
|
# Prune dead eggs from our list.
|
|
self._eggs = [e for e in self._eggs if e]
|
|
|
|
# Spawn more eggs if we've got space.
|
|
if len(self._eggs) < int(self._max_eggs):
|
|
|
|
# Occasionally spawn a land-mine in addition.
|
|
if self._pro_mode and random.random() < 0.25:
|
|
mine = Bomb(
|
|
position=(xpos, ypos, zpos), bomb_type='land_mine'
|
|
).autoretain()
|
|
mine.arm()
|
|
else:
|
|
self._eggs.append(Egg(position=(xpos, ypos, zpos)))
|
|
|
|
# Various high-level game events come through this method.
|
|
def handlemessage(self, msg: Any) -> Any:
|
|
|
|
# Respawn dead players.
|
|
if isinstance(msg, ba.PlayerDiedMessage):
|
|
# Augment standard behavior.
|
|
super().handlemessage(msg)
|
|
|
|
# Respawn them shortly.
|
|
player = msg.getplayer(Player)
|
|
assert self.initialplayerinfos is not None
|
|
respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
|
|
player.respawn_timer = ba.Timer(
|
|
respawn_time, ba.Call(self.spawn_player_if_exists, player)
|
|
)
|
|
player.respawn_icon = RespawnIcon(player, respawn_time)
|
|
|
|
# Whenever our evil bunny dies, respawn him and spew some eggs.
|
|
elif isinstance(msg, SpazBotDiedMessage):
|
|
self._spawn_evil_bunny()
|
|
assert msg.spazbot.node
|
|
pos = msg.spazbot.node.position
|
|
for _i in range(6):
|
|
spread = 0.4
|
|
self._eggs.append(
|
|
Egg(
|
|
position=(
|
|
pos[0] + random.uniform(-spread, spread),
|
|
pos[1] + random.uniform(-spread, spread),
|
|
pos[2] + random.uniform(-spread, spread),
|
|
)
|
|
)
|
|
)
|
|
else:
|
|
# Default handler.
|
|
return super().handlemessage(msg)
|
|
return None
|
|
|
|
def _update_scoreboard(self) -> None:
|
|
for team in self.teams:
|
|
self._scoreboard.set_team_value(team, team.score)
|
|
|
|
def end_game(self) -> None:
|
|
results = ba.GameResults()
|
|
for team in self.teams:
|
|
results.set_team_score(team, team.score)
|
|
self.end(results)
|
|
|
|
|
|
class Egg(ba.Actor):
|
|
"""A lovely egg that can be picked up for points."""
|
|
|
|
def __init__(self, position: tuple[float, float, float] = (0.0, 1.0, 0.0)):
|
|
super().__init__()
|
|
activity = self.activity
|
|
assert isinstance(activity, EasterEggHuntGame)
|
|
shared = SharedObjects.get()
|
|
|
|
# Spawn just above the provided point.
|
|
self._spawn_pos = (position[0], position[1] + 1.0, position[2])
|
|
ctex = (activity.egg_tex_1, activity.egg_tex_2, activity.egg_tex_3)[
|
|
random.randrange(3)
|
|
]
|
|
mats = [shared.object_material, activity.egg_material]
|
|
self.node = ba.newnode(
|
|
'prop',
|
|
delegate=self,
|
|
attrs={
|
|
'model': activity.egg_model,
|
|
'color_texture': ctex,
|
|
'body': 'capsule',
|
|
'reflection': 'soft',
|
|
'model_scale': 0.5,
|
|
'body_scale': 0.6,
|
|
'density': 4.0,
|
|
'reflection_scale': [0.15],
|
|
'shadow_size': 0.6,
|
|
'position': self._spawn_pos,
|
|
'materials': mats,
|
|
},
|
|
)
|
|
|
|
def exists(self) -> bool:
|
|
return bool(self.node)
|
|
|
|
def handlemessage(self, msg: Any) -> Any:
|
|
if isinstance(msg, ba.DieMessage):
|
|
if self.node:
|
|
self.node.delete()
|
|
elif isinstance(msg, ba.HitMessage):
|
|
if self.node:
|
|
assert msg.force_direction is not None
|
|
self.node.handlemessage(
|
|
'impulse',
|
|
msg.pos[0],
|
|
msg.pos[1],
|
|
msg.pos[2],
|
|
msg.velocity[0],
|
|
msg.velocity[1],
|
|
msg.velocity[2],
|
|
1.0 * msg.magnitude,
|
|
1.0 * msg.velocity_magnitude,
|
|
msg.radius,
|
|
0,
|
|
msg.force_direction[0],
|
|
msg.force_direction[1],
|
|
msg.force_direction[2],
|
|
)
|
|
else:
|
|
super().handlemessage(msg)
|