bombsquad-plugin-manager/plugins/minigames/down_into_the_abyss.py
2024-02-01 08:58:54 +00:00

790 lines
28 KiB
Python

# Ported to api 8 by brostos using baport.(https://github.com/bombsquad-community/baport)
# ba_meta require api 8
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
from typing import TYPE_CHECKING
import babase
import bauiv1 as bui
import bascenev1 as bs
import _babase
import random
from bascenev1._map import register_map
from bascenev1lib.actor.spaz import PickupMessage
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.spazfactory import SpazFactory
from bascenev1lib.gameutils import SharedObjects
from bascenev1lib.actor.spazbot import SpazBotSet, ChargerBotPro, TriggerBotPro
from bascenev1lib.actor.bomb import Blast
from bascenev1lib.actor.powerupbox import PowerupBoxFactory
from bascenev1lib.actor.onscreentimer import OnScreenTimer
if TYPE_CHECKING:
from typing import Any, Sequence
lang = bs.app.lang.language
if lang == 'Spanish':
name = 'Abajo en el Abismo'
description = 'Sobrevive tanto como puedas'
help = 'El mapa es 3D, ¡ten cuidado!'
author = 'Autor: Deva'
github = 'GitHub: spdv123'
blog = 'Blog: superdeva.info'
peaceTime = 'Tiempo de Paz'
npcDensity = 'Densidad de Enemigos'
hint_use_punch = '¡Ahora puedes golpear a los enemigos!'
elif lang == 'Chinese':
name = '无尽深渊'
description = '在无穷尽的坠落中存活更长时间'
help = ''
author = '作者: Deva'
github = 'GitHub: spdv123'
blog = '博客: superdeva.info'
peaceTime = '和平时间'
npcDensity = 'NPC密度'
hint_use_punch = u'现在可以使用拳头痛扁你的敌人了'
else:
name = 'Down Into The Abyss'
description = 'Survive as long as you can'
help = 'The map is 3D, be careful!'
author = 'Author: Deva'
github = 'GitHub: spdv123'
blog = 'Blog: superdeva.info'
peaceTime = 'Peace Time'
npcDensity = 'NPC Density'
hint_use_punch = 'You can punch your enemies now!'
class AbyssMap(bs.Map):
from bascenev1lib.mapdata import happy_thoughts as defs
# Add the y-dimension space for players
defs.boxes['map_bounds'] = (-0.8748348681, 9.212941713, -9.729538885) \
+ (0.0, 0.0, 0.0) \
+ (36.09666006, 26.19950145, 20.89541168)
name = 'Abyss Unhappy'
@classmethod
def get_play_types(cls) -> list[str]:
"""Return valid play types for this map."""
return ['abyss']
@classmethod
def get_preview_texture_name(cls) -> str:
return 'alwaysLandPreview'
@classmethod
def on_preload(cls) -> Any:
data: dict[str, Any] = {
'mesh': bs.getmesh('alwaysLandLevel'),
'bottom_mesh': bs.getmesh('alwaysLandLevelBottom'),
'bgmesh': bs.getmesh('alwaysLandBG'),
'collision_mesh': bs.getcollisionmesh('alwaysLandLevelCollide'),
'tex': bs.gettexture('alwaysLandLevelColor'),
'bgtex': bs.gettexture('alwaysLandBGColor'),
'vr_fill_mound_mesh': bs.getmesh('alwaysLandVRFillMound'),
'vr_fill_mound_tex': bs.gettexture('vrFillMound')
}
return data
@classmethod
def get_music_type(cls) -> bs.MusicType:
return bs.MusicType.FLYING
def __init__(self) -> None:
super().__init__(vr_overlay_offset=(0, -3.7, 2.5))
self.background = bs.newnode(
'terrain',
attrs={
'mesh': self.preloaddata['bgmesh'],
'lighting': False,
'background': True,
'color_texture': self.preloaddata['bgtex']
})
bs.newnode('terrain',
attrs={
'mesh': self.preloaddata['vr_fill_mound_mesh'],
'lighting': False,
'vr_only': True,
'color': (0.2, 0.25, 0.2),
'background': True,
'color_texture': self.preloaddata['vr_fill_mound_tex']
})
gnode = bs.getactivity().globalsnode
gnode.happy_thoughts_mode = True
gnode.shadow_offset = (0.0, 8.0, 5.0)
gnode.tint = (1.3, 1.23, 1.0)
gnode.ambient_color = (1.3, 1.23, 1.0)
gnode.vignette_outer = (0.64, 0.59, 0.69)
gnode.vignette_inner = (0.95, 0.95, 0.93)
gnode.vr_near_clip = 1.0
self.is_flying = True
register_map(AbyssMap)
class SpazTouchFoothold:
pass
class BombToDieMessage:
pass
class Foothold(bs.Actor):
def __init__(self,
position: Sequence[float] = (0.0, 1.0, 0.0),
power: str = 'random',
size: float = 6.0,
breakable: bool = True,
moving: bool = False):
super().__init__()
shared = SharedObjects.get()
powerup = PowerupBoxFactory.get()
fmesh = bs.getmesh('landMine')
fmeshs = bs.getmesh('powerupSimple')
self.died = False
self.breakable = breakable
self.moving = moving # move right and left
self.lrSig = 1 # left or right signal
self.lrSpeedPlus = random.uniform(1 / 2.0, 1 / 0.7)
self._npcBots = SpazBotSet()
self.foothold_material = bs.Material()
self.impact_sound = bui.getsound('impactMedium')
self.foothold_material.add_actions(
conditions=(('they_dont_have_material', shared.player_material),
'and',
('they_have_material', shared.object_material),
'or',
('they_have_material', shared.footing_material)),
actions=(('modify_node_collision', 'collide', True),
))
self.foothold_material.add_actions(
conditions=('they_have_material', shared.player_material),
actions=(('modify_part_collision', 'physical', True),
('modify_part_collision', 'stiffness', 0.05),
('message', 'our_node', 'at_connect', SpazTouchFoothold()),
))
self.foothold_material.add_actions(
conditions=('they_have_material', self.foothold_material),
actions=('modify_node_collision', 'collide', False),
)
tex = {
'punch': powerup.tex_punch,
'sticky_bombs': powerup.tex_sticky_bombs,
'ice_bombs': powerup.tex_ice_bombs,
'impact_bombs': powerup.tex_impact_bombs,
'health': powerup.tex_health,
'curse': powerup.tex_curse,
'shield': powerup.tex_shield,
'land_mines': powerup.tex_land_mines,
'tnt': bs.gettexture('tnt'),
}.get(power, bs.gettexture('tnt'))
powerupdist = {
powerup.tex_bomb: 3,
powerup.tex_ice_bombs: 2,
powerup.tex_punch: 3,
powerup.tex_impact_bombs: 3,
powerup.tex_land_mines: 3,
powerup.tex_sticky_bombs: 4,
powerup.tex_shield: 4,
powerup.tex_health: 3,
powerup.tex_curse: 1,
bs.gettexture('tnt'): 2
}
self.randtex = []
for keyTex in powerupdist:
for i in range(powerupdist[keyTex]):
self.randtex.append(keyTex)
if power == 'random':
random.seed()
tex = random.choice(self.randtex)
self.tex = tex
self.powerup_type = {
powerup.tex_punch: 'punch',
powerup.tex_bomb: 'triple_bombs',
powerup.tex_ice_bombs: 'ice_bombs',
powerup.tex_impact_bombs: 'impact_bombs',
powerup.tex_land_mines: 'land_mines',
powerup.tex_sticky_bombs: 'sticky_bombs',
powerup.tex_shield: 'shield',
powerup.tex_health: 'health',
powerup.tex_curse: 'curse',
bs.gettexture('tnt'): 'tnt'
}.get(self.tex, '')
self._spawn_pos = (position[0], position[1], position[2])
self.node = bs.newnode(
'prop',
delegate=self,
attrs={
'body': 'landMine',
'position': self._spawn_pos,
'mesh': fmesh,
'light_mesh': fmeshs,
'shadow_size': 0.5,
'velocity': (0, 0, 0),
'density': 90000000000,
'sticky': False,
'body_scale': size,
'mesh_scale': size,
'color_texture': tex,
'reflection': 'powerup',
'is_area_of_interest': True,
'gravity_scale': 0.0,
'reflection_scale': [0],
'materials': [self.foothold_material,
shared.object_material,
shared.footing_material]
})
self.touchedSpazs = set()
self.keep_vel()
def keep_vel(self) -> None:
if self.node and not self.died:
speed = bs.getactivity().cur_speed
if self.moving:
if abs(self.node.position[0]) > 10:
self.lrSig *= -1
self.node.velocity = (
self.lrSig * speed * self.lrSpeedPlus, speed, 0)
bs.timer(0.1, bs.WeakCall(self.keep_vel))
else:
self.node.velocity = (0, speed, 0)
# self.node.extraacceleration = (0, self.speed, 0)
bs.timer(0.1, bs.WeakCall(self.keep_vel))
def tnt_explode(self) -> None:
pos = self.node.position
Blast(position=pos,
blast_radius=6.0,
blast_type='tnt',
source_player=None).autoretain()
def spawn_npc(self) -> None:
if not self.breakable:
return
if self._npcBots.have_living_bots():
return
if random.randint(0, 3) >= bs.getactivity().npc_density:
return
pos = self.node.position
pos = (pos[0], pos[1] + 1, pos[2])
self._npcBots.spawn_bot(
bot_type=random.choice([ChargerBotPro, TriggerBotPro]),
pos=pos,
spawn_time=10)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.DieMessage):
if self.node:
self.node.delete()
self.died = True
elif isinstance(msg, bs.OutOfBoundsMessage):
self.handlemessage(bs.DieMessage())
elif isinstance(msg, BombToDieMessage):
if self.powerup_type == 'tnt':
self.tnt_explode()
self.handlemessage(bs.DieMessage())
elif isinstance(msg, bs.HitMessage):
ispunched = (msg.srcnode and msg.srcnode.getnodetype() == 'spaz')
if not ispunched:
if self.breakable:
self.handlemessage(BombToDieMessage())
elif isinstance(msg, SpazTouchFoothold):
node = bs.getcollision().opposingnode
if node is not None and node:
try:
spaz = node.getdelegate(object)
if not isinstance(spaz, AbyssPlayerSpaz):
return
if spaz in self.touchedSpazs:
return
self.touchedSpazs.add(spaz)
self.spawn_npc()
spaz.fix_2D_position()
if self.powerup_type not in ['', 'tnt']:
node.handlemessage(
bs.PowerupMessage(self.powerup_type))
except Exception as e:
print(e)
pass
class AbyssPlayerSpaz(PlayerSpaz):
def __init__(self,
player: bs.Player,
color: Sequence[float] = (1.0, 1.0, 1.0),
highlight: Sequence[float] = (0.5, 0.5, 0.5),
character: str = 'Spaz',
powerups_expire: bool = True):
super().__init__(player=player,
color=color,
highlight=highlight,
character=character,
powerups_expire=powerups_expire)
self.node.fly = False
self.node.hockey = True
self.hitpoints_max = self.hitpoints = 1500 # more HP to handle drop
bs.timer(bs.getactivity().peace_time,
bs.WeakCall(self.safe_connect_controls_to_player))
def safe_connect_controls_to_player(self) -> None:
try:
self.connect_controls_to_player()
except:
pass
def on_move_up_down(self, value: float) -> None:
"""
Called to set the up/down joystick amount on this spaz;
used for player or AI connections.
value will be between -32768 to 32767
WARNING: deprecated; use on_move instead.
"""
if not self.node:
return
if self.node.run > 0.1:
self.node.move_up_down = value
else:
self.node.move_up_down = value / 3.
def on_move_left_right(self, value: float) -> None:
"""
Called to set the left/right joystick amount on this spaz;
used for player or AI connections.
value will be between -32768 to 32767
WARNING: deprecated; use on_move instead.
"""
if not self.node:
return
if self.node.run > 0.1:
self.node.move_left_right = value
else:
self.node.move_left_right = value / 1.5
def fix_2D_position(self) -> None:
self.node.fly = True
bs.timer(0.02, bs.WeakCall(self.disable_fly))
def disable_fly(self) -> None:
if self.node:
self.node.fly = False
def curse(self) -> None:
"""
Give this poor spaz a curse;
he will explode in 5 seconds.
"""
if not self._cursed:
factory = SpazFactory.get()
self._cursed = True
# Add the curse material.
for attr in ['materials', 'roller_materials']:
materials = getattr(self.node, attr)
if factory.curse_material not in materials:
setattr(self.node, attr,
materials + (factory.curse_material, ))
# None specifies no time limit
assert self.node
if self.curse_time == -1:
self.node.curse_death_time = -1
else:
# Note: curse-death-time takes milliseconds.
tval = bs.time()
assert isinstance(tval, (float, int))
self.node.curse_death_time = bs.time() + 15
bs.timer(15, bs.WeakCall(self.curse_explode))
def handlemessage(self, msg: Any) -> Any:
dontUp = False
if isinstance(msg, PickupMessage):
dontUp = True
collision = bs.getcollision()
opposingnode = collision.opposingnode
opposingbody = collision.opposingbody
if opposingnode is None or not opposingnode:
return True
opposingdelegate = opposingnode.getdelegate(object)
# Don't pick up the foothold
if isinstance(opposingdelegate, Foothold):
return True
# dont allow picking up of invincible dudes
try:
if opposingnode.invincible:
return True
except Exception:
pass
# if we're grabbing the pelvis of a non-shattered spaz,
# we wanna grab the torso instead
if (opposingnode.getnodetype() == 'spaz'
and not opposingnode.shattered and opposingbody == 4):
opposingbody = 1
# Special case - if we're holding a flag, don't replace it
# (hmm - should make this customizable or more low level).
held = self.node.hold_node
if held and held.getnodetype() == 'flag':
return True
# Note: hold_body needs to be set before hold_node.
self.node.hold_body = opposingbody
self.node.hold_node = opposingnode
if not dontUp:
PlayerSpaz.handlemessage(self, msg)
class Player(bs.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
super().__init__()
self.death_time: float | None = None
self.notIn: bool = None
class Team(bs.Team[Player]):
"""Our team type for this game."""
# ba_meta export bascenev1.GameActivity
class AbyssGame(bs.TeamGameActivity[Player, Team]):
name = name
description = description
scoreconfig = bs.ScoreConfig(label='Survived',
scoretype=bs.ScoreType.MILLISECONDS,
version='B')
# Print messages when players die (since its meaningful in this game).
announce_player_deaths = True
# We're currently hard-coded for one map.
@classmethod
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
return ['Abyss Unhappy']
@classmethod
def get_available_settings(
cls, sessiontype: type[bs.Session]) -> list[babase.Setting]:
settings = [
bs.FloatChoiceSetting(
peaceTime,
choices=[
('None', 0.0),
('Shorter', 2.5),
('Short', 5.0),
('Normal', 10.0),
('Long', 15.0),
('Longer', 20.0),
],
default=10.0,
),
bs.FloatChoiceSetting(
npcDensity,
choices=[
('0%', 0),
('25%', 1),
('50%', 2),
('75%', 3),
('100%', 4),
],
default=2,
),
bs.BoolSetting('Epic Mode', default=False),
]
return settings
# We support teams, free-for-all, and co-op sessions.
@classmethod
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
return (issubclass(sessiontype, bs.DualTeamSession)
or issubclass(sessiontype, bs.FreeForAllSession)
or issubclass(sessiontype, bs.CoopSession))
def __init__(self, settings: dict):
super().__init__(settings)
self._epic_mode = settings.get('Epic Mode', False)
self._last_player_death_time: float | None = None
self._timer: OnScreenTimer | None = None
self.fix_y = -5.614479365
self.start_z = 0
self.init_position = (0, self.start_z, self.fix_y)
self.team_init_positions = [(-5, self.start_z, self.fix_y),
(5, self.start_z, self.fix_y)]
self.cur_speed = 2.5
# TODO: The variable below should be set in settings
self.peace_time = float(settings[peaceTime])
self.npc_density = float(settings[npcDensity])
# Some base class overrides:
self.default_music = (bs.MusicType.EPIC
if self._epic_mode else bs.MusicType.SURVIVAL)
if self._epic_mode:
self.slow_motion = True
self._game_credit = bs.NodeActor(
bs.newnode(
'text',
attrs={
'v_attach': 'bottom',
'h_align': 'center',
'vr_depth': 0,
'color': (0.0, 0.7, 1.0),
'shadow': 1.0 if True else 0.5,
'flatness': 1.0 if True else 0.5,
'position': (0, 0),
'scale': 0.8,
'text': ' | '.join([author, github, blog])
}))
def get_instance_description(self) -> str | Sequence:
return description
def get_instance_description_short(self) -> str | Sequence:
return self.get_instance_description() + '\n' + help
def on_player_join(self, player: Player) -> None:
if self.has_begun():
player.notIn = True
bs.broadcastmessage(babase.Lstr(
resource='playerDelayedJoinText',
subs=[('${PLAYER}', player.getname(full=True))]),
color=(0, 1, 0))
self.spawn_player(player)
def on_begin(self) -> None:
super().on_begin()
self._timer = OnScreenTimer()
self._timer.start()
self.level_cnt = 1
if self.teams_or_ffa() == 'teams':
ip0 = self.team_init_positions[0]
ip1 = self.team_init_positions[1]
Foothold(
(ip0[0], ip0[1] - 2, ip0[2]),
power='shield', breakable=False).autoretain()
Foothold(
(ip1[0], ip1[1] - 2, ip1[2]),
power='shield', breakable=False).autoretain()
else:
ip = self.init_position
Foothold(
(ip[0], ip[1] - 2, ip[2]),
power='shield', breakable=False).autoretain()
bs.timer(int(5.0 / self.cur_speed),
bs.WeakCall(self.add_foothold), repeat=True)
# Repeat check game end
bs.timer(1.0, self._check_end_game, repeat=True)
bs.timer(self.peace_time + 0.1,
bs.WeakCall(self.tip_hint, hint_use_punch))
bs.timer(6.0, bs.WeakCall(self.faster_speed), repeat=True)
def tip_hint(self, text: str) -> None:
bs.broadcastmessage(text, color=(0.2, 0.2, 1))
def faster_speed(self) -> None:
self.cur_speed *= 1.15
def add_foothold(self) -> None:
ip = self.init_position
ip_1 = (ip[0] - 7, ip[1], ip[2])
ip_2 = (ip[0] + 7, ip[1], ip[2])
ru = random.uniform
self.level_cnt += 1
if self.level_cnt % 3:
Foothold((
ip_1[0] + ru(-5, 5),
ip[1] - 2,
ip[2] + ru(-0.0, 0.0))).autoretain()
Foothold((
ip_2[0] + ru(-5, 5),
ip[1] - 2,
ip[2] + ru(-0.0, 0.0))).autoretain()
else:
Foothold((
ip[0] + ru(-8, 8),
ip[1] - 2,
ip[2]), moving=True).autoretain()
def teams_or_ffa(self) -> None:
if isinstance(self.session, bs.DualTeamSession):
return 'teams'
return 'ffa'
def spawn_player_spaz(self,
player: Player,
position: Sequence[float] = (0, 0, 0),
angle: float | None = None) -> PlayerSpaz:
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from babase import _math
from bascenev1._gameutils import animate
position = self.init_position
if self.teams_or_ffa() == 'teams':
position = self.team_init_positions[player.team.id % 2]
angle = None
name = player.getname()
color = player.color
highlight = player.highlight
light_color = _math.normalized_color(color)
display_color = _babase.safecolor(color, target_intensity=0.75)
spaz = AbyssPlayerSpaz(color=color,
highlight=highlight,
character=player.character,
player=player)
player.actor = spaz
assert spaz.node
spaz.node.name = name
spaz.node.name_color = display_color
spaz.connect_controls_to_player(enable_punch=False,
enable_bomb=True,
enable_pickup=False)
# Move to the stand position and add a flash of light.
spaz.handlemessage(
bs.StandMessage(
position,
angle if angle is not None else random.uniform(0, 360)))
self._spawn_sound.play(1, position=spaz.node.position)
light = bs.newnode('light', attrs={'color': light_color})
spaz.node.connectattr('position', light, 'position')
animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0})
bs.timer(0.5, light.delete)
return spaz
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, bs.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
curtime = bs.time()
# Record the player's moment of death.
# assert isinstance(msg.spaz.player
msg.getplayer(Player).death_time = curtime
# In co-op mode, end the game the instant everyone dies
# (more accurate looking).
# In teams/ffa, allow a one-second fudge-factor so we can
# get more draws if players die basically at the same time.
if isinstance(self.session, bs.CoopSession):
# Teams will still show up if we check now.. check in
# the next cycle.
babase.pushcall(self._check_end_game)
# Also record this for a final setting of the clock.
self._last_player_death_time = curtime
else:
bs.timer(1.0, self._check_end_game)
else:
# Default handler:
return super().handlemessage(msg)
return None
def _check_end_game(self) -> None:
living_team_count = 0
for team in self.teams:
for player in team.players:
if player.is_alive():
living_team_count += 1
break
# In co-op, we go till everyone is dead.. otherwise we go
# until one team remains.
if isinstance(self.session, bs.CoopSession):
if living_team_count <= 0:
self.end_game()
else:
if living_team_count <= 0:
self.end_game()
def end_game(self) -> None:
cur_time = bs.time()
assert self._timer is not None
start_time = self._timer.getstarttime()
# Mark death-time as now for any still-living players
# and award players points for how long they lasted.
# (these per-player scores are only meaningful in team-games)
for team in self.teams:
for player in team.players:
survived = False
if player.notIn:
player.death_time = 0
# Throw an extra fudge factor in so teams that
# didn't die come out ahead of teams that did.
if player.death_time is None:
survived = True
player.death_time = cur_time + 1
# Award a per-player score depending on how many seconds
# they lasted (per-player scores only affect teams mode;
# everywhere else just looks at the per-team score).
score = int(player.death_time - self._timer.getstarttime())
if survived:
score += 50 # A bit extra for survivors.
self.stats.player_scored(player, score, screenmessage=False)
# 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.
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:
# Set the team score to the max time survived by any player on
# that team.
longest_life = 0.0
for player in team.players:
assert player.death_time is not None
longest_life = max(longest_life,
player.death_time - start_time)
# Submit the score value in milliseconds.
results.set_team_score(team, int(1000.0 * longest_life))
self.end(results=results)