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

991 lines
35 KiB
Python
Raw Normal View History

2024-01-17 23:09:18 +03:00
"""UFO Boss Fight v2.0:
2023-05-15 15:56:45 +05:30
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
2024-01-17 23:09:18 +03:00
# ba_meta require api 8
2023-05-15 15:56:45 +05:30
# (see https://ballistica.net/wiki/meta-tag-system)
2024-01-17 23:09:18 +03:00
# ---------------------------------------
# Update v2.0
# updated to api 8
# ---------------------------------------
2023-05-15 15:56:45 +05:30
from __future__ import annotations
import random
from typing import TYPE_CHECKING
2024-01-17 23:09:18 +03:00
import babase
import bascenev1 as bs
from bascenev1lib.actor.playerspaz import PlayerSpaz
from bascenev1lib.actor.spaz import Spaz
from bascenev1lib.actor.bomb import Blast, Bomb
from bascenev1lib.actor.onscreentimer import OnScreenTimer
from bascenev1lib.actor.spazbot import SpazBotSet, StickyBot
from bascenev1lib.gameutils import SharedObjects
2023-05-15 15:56:45 +05:30
if TYPE_CHECKING:
from typing import Any, Sequence, Union, Callable
class UFODiedMessage:
ufo: UFO
"""The UFO that was killed."""
2024-01-17 23:09:18 +03:00
killerplayer: bs.Player | None
"""The bs.Player that killed it (or None)."""
2023-05-15 15:56:45 +05:30
2024-01-17 23:09:18 +03:00
how: bs.DeathType
2023-05-15 15:56:45 +05:30
"""The particular type of death."""
def __init__(
self,
ufo: UFO,
2024-01-17 23:09:18 +03:00
killerplayer: bs.Player | None,
how: bs.DeathType,
2023-05-15 15:56:45 +05:30
):
"""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)
2024-01-17 23:09:18 +03:00
class UFO(bs.Actor):
2023-05-15 15:56:45 +05:30
"""
New AI for Boss
"""
# pylint: disable=too-many-public-methods
# pylint: disable=too-many-locals
2024-01-17 23:09:18 +03:00
node: bs.Node
2023-05-15 15:56:45 +05:30
def __init__(self, hitpoints: int = 5000):
super().__init__()
shared = SharedObjects.get()
self.update_callback: Callable[[UFO], Any] | None = None
activity = self.activity
2024-01-17 23:09:18 +03:00
assert isinstance(activity, bs.GameActivity)
2023-05-15 15:56:45 +05:30
2024-01-17 23:09:18 +03:00
self.platform_material = bs.Material()
2023-05-15 15:56:45 +05:30
self.platform_material.add_actions(
conditions=('they_have_material', shared.footing_material),
actions=(
'modify_part_collision', 'collide', True))
2024-01-17 23:09:18 +03:00
self.ice_material = bs.Material()
2023-05-15 15:56:45 +05:30
self.ice_material.add_actions(
actions=('modify_part_collision', 'friction', 0.0))
2024-01-17 23:09:18 +03:00
self._player_pts: list[tuple[bs.Vec3, bs.Vec3]] | None = None
self._ufo_update_timer: bs.Timer | None = None
self.last_player_attacked_by: bs.Player | None = None
2023-05-15 15:56:45 +05:30
self.last_attacked_time = 0.0
self.last_attacked_type: tuple[str, str] | None = None
2024-01-17 23:09:18 +03:00
self.to_target: bs.Vec3 = bs.Vec3(0, 0, 0)
2023-05-15 15:56:45 +05:30
self.dist = (0, 0, 0)
self._bots = SpazBotSet()
self.frozen = False
self.bot_count = 3
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
2024-01-17 23:09:18 +03:00
self._bar_tex = self._backing_tex = bs.gettexture('bar')
self._cover_tex = bs.gettexture('uiAtlas')
self._mesh = bs.getmesh('meterTransparent')
2023-05-15 15:56:45 +05:30
self.bar_posx = -120
self._last_hit_time: int | None = None
self.impact_scale = 1.0
self._num_times_hit = 0
2024-01-17 23:09:18 +03:00
self._sucker_mat = bs.Material()
2023-05-15 15:56:45 +05:30
2024-01-17 23:09:18 +03:00
self.ufo_material = bs.Material()
2023-05-15 15:56:45 +05:30
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))
2024-01-17 23:09:18 +03:00
activity = bs.get_foreground_host_activity()
point = activity.map.get_flag_position(None)
boss_spawn_pos = (point[0], point[1] + 1, point[2])
2023-05-15 15:56:45 +05:30
2024-01-17 23:09:18 +03:00
self.node = bs.newnode('prop', delegate=self, attrs={
2023-05-15 15:56:45 +05:30
'position': boss_spawn_pos,
'velocity': (2, 0, 0),
2024-01-17 23:09:18 +03:00
'color_texture': bs.gettexture('achievementFootballShutout'),
'mesh': bs.getmesh('landMine'),
# 'light_mesh': bs.getmesh('powerupSimple'),
'mesh_scale': 3.3,
2023-05-15 15:56:45 +05:30
'body': 'landMine',
'body_scale': 3.3,
2024-01-17 23:09:18 +03:00
'gravity_scale': 0.2,
2023-05-15 15:56:45 +05:30
'density': 1,
'reflection': 'soft',
'reflection_scale': [0.25],
'shadow_size': 0.1,
'max_speed': 1.5,
'is_area_of_interest':
2024-01-17 23:09:18 +03:00
True,
2023-05-15 15:56:45 +05:30
'materials': [shared.footing_material, shared.object_material]})
2024-01-17 23:09:18 +03:00
self.holder = bs.newnode('region', attrs={
2023-05-15 15:56:45 +05:30
'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)})
2024-01-17 23:09:18 +03:00
self.suck_anim = bs.newnode('locator',
2023-05-15 15:56:45 +05:30
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():
2024-01-17 23:09:18 +03:00
bs.animate_array(self.suck_anim, 'position', 3,
2023-05-15 15:56:45 +05:30
{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)})
2024-01-17 23:09:18 +03:00
self.suck_timer = bs.Timer(0.5, suck_anim, repeat=True)
2023-05-15 15:56:45 +05:30
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)
))
2024-01-17 23:09:18 +03:00
# self.sucker = bs.newnode('region', attrs={
2023-05-15 15:56:45 +05:30
# 'position': (
# boss_spawn_pos[0], boss_spawn_pos[1] - 2, boss_spawn_pos[2]),
# 'scale': [2, 10, 2],
# 'type': 'box',
# 'materials': self._sucker_mat, })
2024-01-17 23:09:18 +03:00
self.suck = bs.newnode('region',
2023-05-15 15:56:45 +05:30
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')
2024-01-17 23:09:18 +03:00
bs.animate(self.node, 'mesh_scale', {
2023-05-15 15:56:45 +05:30
0: 0,
2024-01-17 23:09:18 +03:00
0.2: self.node.mesh_scale * 1.1,
0.26: self.node.mesh_scale})
2023-05-15 15:56:45 +05:30
2024-01-17 23:09:18 +03:00
self.shield_deco = bs.newnode('shield', owner=self.node,
2023-05-15 15:56:45 +05:30
attrs={'color': (4, 4, 4),
'radius': 1.2})
self.node.connectattr('position', self.shield_deco, 'position')
self._scoreboard()
self._update()
2024-01-17 23:09:18 +03:00
self.drop_bomb_timer = bs.Timer(1.5, bs.Call(self._drop_bomb),
2023-05-15 15:56:45 +05:30
repeat=True)
2024-01-17 23:09:18 +03:00
self.drop_bots_timer = bs.Timer(15.0, bs.Call(self._drop_bots), repeat=True)
2023-05-15 15:56:45 +05:30
def _drop_bots(self) -> None:
p = self.node.position
2024-01-17 23:09:18 +03:00
for i in range(self.bot_count):
bs.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
),
)
2023-05-15 15:56:45 +05:30
def _drop_bomb(self) -> None:
t = self.to_target
p = self.node.position
2024-01-17 23:09:18 +03:00
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()
2023-05-15 15:56:45 +05:30
def _levitate(self):
2024-01-17 23:09:18 +03:00
node = bs.getcollision().opposingnode
2023-05-15 15:56:45 +05:30
if node.exists():
p = node.getdelegate(Spaz, True)
2024-01-17 23:09:18 +03:00
def raise_player(player: bs.Player):
2023-05-15 15:56:45 +05:30
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)
2024-01-17 23:09:18 +03:00
2023-05-15 15:56:45 +05:30
except:
pass
if not self.frozen:
for i in range(7):
2024-01-17 23:09:18 +03:00
bs.timer(0.05 + i / 20, bs.Call(raise_player, p))
2023-05-15 15:56:45 +05:30
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
self.hitpoints -= int(damage)
if self.hitpoints <= 0:
2024-01-17 23:09:18 +03:00
self.handlemessage(bs.DieMessage())
2023-05-15 15:56:45 +05:30
def _get_target_player_pt(self) -> tuple[
2024-01-17 23:09:18 +03:00
bs.Vec3 | None, bs.Vec3 | None]:
2023-05-15 15:56:45 +05:30
"""Returns the position and velocity of our target.
Both values will be None in the case of no target.
"""
assert self.node
2024-01-17 23:09:18 +03:00
botpt = bs.Vec3(self.node.position)
2023-05-15 15:56:45 +05:30
closest_dist: float | None = None
2024-01-17 23:09:18 +03:00
closest_vel: bs.Vec3 | None = None
closest: bs.Vec3 | None = None
2023-05-15 15:56:45 +05:30
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 (
2024-01-17 23:09:18 +03:00
bs.Vec3(closest[0], closest[1], closest[2]),
bs.Vec3(closest_vel[0], closest_vel[1], closest_vel[2]),
2023-05-15 15:56:45 +05:30
)
return None, None
2024-01-17 23:09:18 +03:00
def set_player_points(self, pts: list[tuple[bs.Vec3, bs.Vec3]]) -> None:
2023-05-15 15:56:45 +05:30
"""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
2024-01-17 23:09:18 +03:00
app = bs.app
2023-05-15 15:56:45 +05:30
# FIXME: Should never vary game elements based on local config.
# (connected clients may have differing configs so they won't
# get the intended results).
2024-01-17 23:09:18 +03:00
do_big = app.ui.uiscale is bs.UIScale.SMALL or app.vr_mode
txtnode = bs.newnode('text',
2023-05-15 15:56:45 +05:30
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.
2024-01-17 23:09:18 +03:00
tcombine = bs.newnode('combine', owner=txtnode, attrs={'size': 3})
2023-05-15 15:56:45 +05:30
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]
2024-01-17 23:09:18 +03:00
bs.animate(tcombine, 'input0',
2023-05-15 15:56:45 +05:30
{i[0] * lifespan: p_start + p_dir * i[1]
for i in v_vals})
p_start = position[1]
p_dir = direction[1]
2024-01-17 23:09:18 +03:00
bs.animate(tcombine, 'input1',
2023-05-15 15:56:45 +05:30
{i[0] * lifespan: p_start + p_dir * i[1]
for i in v_vals})
p_start = position[2]
p_dir = direction[2]
2024-01-17 23:09:18 +03:00
bs.animate(tcombine, 'input2',
2023-05-15 15:56:45 +05:30
{i[0] * lifespan: p_start + p_dir * i[1]
for i in v_vals})
2024-01-17 23:09:18 +03:00
bs.animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0})
bs.timer(lifespan, txtnode.delete)
2023-05-15 15:56:45 +05:30
def _scoreboard(self) -> None:
2024-01-17 23:09:18 +03:00
self._backing = bs.NodeActor(
bs.newnode('image',
2023-05-15 15:56:45 +05:30
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
}))
2024-01-17 23:09:18 +03:00
self._bar = bs.NodeActor(
bs.newnode('image',
2023-05-15 15:56:45 +05:30
attrs={
'opacity': 1.0,
'color': (0.5, 0.5, 0.5),
'attach': 'topCenter',
'texture': self._bar_tex
}))
2024-01-17 23:09:18 +03:00
self._bar_scale = bs.newnode('combine',
2023-05-15 15:56:45 +05:30
owner=self._bar.node,
attrs={
'size': 2,
'input0': self._bar_width,
'input1': self._bar_height
})
self._bar_scale.connectattr('output', self._bar.node, 'scale')
2024-01-17 23:09:18 +03:00
self._bar_position = bs.newnode(
2023-05-15 15:56:45 +05:30
'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')
2024-01-17 23:09:18 +03:00
self._cover = bs.NodeActor(
bs.newnode('image',
2023-05-15 15:56:45 +05:30
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,
2024-01-17 23:09:18 +03:00
'mesh_transparent': self._mesh
2023-05-15 15:56:45 +05:30
}))
2024-01-17 23:09:18 +03:00
self._score_text = bs.NodeActor(
bs.newnode('text',
2023-05-15 15:56:45 +05:30
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
2024-01-17 23:09:18 +03:00
bs.animate(self._bar_scale, 'input0', {
2023-05-15 15:56:45 +05:30
0.0: cur_width,
0.1: self._bar_width
})
cur_x = self._bar_position.input0
2024-01-17 23:09:18 +03:00
bs.animate(self._bar_position, 'input0', {
2023-05-15 15:56:45 +05:30
0.0: cur_x,
0.1: self.bar_posx + self._bar_width / 2
})
if self.hitpoints > self.hitpoints_max * 3 / 4:
2024-01-17 23:09:18 +03:00
bs.animate_array(self.shield_deco, 'color', 3,
2023-05-15 15:56:45 +05:30
{0: self.shield_deco.color, 0.2: (4, 4, 4)})
elif self.hitpoints > self.hitpoints_max * 1 / 2:
2024-01-17 23:09:18 +03:00
bs.animate_array(self.shield_deco, 'color', 3,
2023-05-15 15:56:45 +05:30
{0: self.shield_deco.color, 0.2: (3, 3, 5)})
self.bot_count = 4
elif self.hitpoints > self.hitpoints_max * 1 / 4:
2024-01-17 23:09:18 +03:00
bs.animate_array(self.shield_deco, 'color', 3,
2023-05-15 15:56:45 +05:30
{0: self.shield_deco.color, 0.2: (1, 5, 1)})
self.bot_count = 5
else:
2024-01-17 23:09:18 +03:00
bs.animate_array(self.shield_deco, 'color', 3,
2023-05-15 15:56:45 +05:30
{0: self.shield_deco.color, 0.2: (5, 0.2, 0.2)})
self.bot_count = 6
2024-01-17 23:09:18 +03:00
2023-05-15 15:56:45 +05:30
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
2024-01-17 23:09:18 +03:00
our_pos = bs.Vec3(pos[0], pos[1] - 3, pos[2])
2023-05-15 15:56:45 +05:30
2024-01-17 23:09:18 +03:00
target_pt_raw: bs.Vec3 | None
target_vel: bs.Vec3 | None
2023-05-15 15:56:45 +05:30
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))
2024-01-17 23:09:18 +03:00
elif not self.frozen:
2023-05-15 15:56:45 +05:30
setattr(self.node, 'velocity',
2024-01-17 23:09:18 +03:00
(self.to_target.x, self.to_target.y, self.to_target.z))
2023-05-15 15:56:45 +05:30
setattr(self.node, 'extra_acceleration',
2024-01-17 23:09:18 +03:00
(self.to_target.x, self.to_target.y * 80 + 70,
self.to_target.z))
2023-05-15 15:56:45 +05:30
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
2024-01-17 23:09:18 +03:00
def animate_mesh(self) -> None:
2023-05-15 15:56:45 +05:30
if not self.node:
return None
2024-01-17 23:09:18 +03:00
# bs.animate(self.node, 'mesh_scale', {
# 0: self.node.mesh_scale,
# 0.08: self.node.mesh_scale * 0.9,
# 0.15: self.node.mesh_scale})
bs.emitfx(position=self.node.position,
2023-05-15 15:56:45 +05:30
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
2024-01-17 23:09:18 +03:00
if isinstance(msg, bs.HitMessage):
2023-05-15 15:56:45 +05:30
# Don't die on punches (that's annoying).
2024-01-17 23:09:18 +03:00
self.animate_mesh()
2023-05-15 15:56:45 +05:30
if self.hitpoints != 0:
self.do_damage(msg)
# self.show_damage_msg(msg)
self._update()
2024-01-17 23:09:18 +03:00
elif isinstance(msg, bs.DieMessage):
2023-05-15 15:56:45 +05:30
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()
2024-01-17 23:09:18 +03:00
bs.timer(0 + i, bs.Call(ded_explode, i))
2023-05-15 15:56:45 +05:30
2024-01-17 23:09:18 +03:00
bs.timer(5, self.node.delete)
bs.timer(0.1, self.suck.delete)
bs.timer(0.1, self.suck_anim.delete)
2023-05-15 15:56:45 +05:30
2024-01-17 23:09:18 +03:00
elif isinstance(msg, bs.OutOfBoundsMessage):
activity = bs.get_foreground_host_activity()
2023-05-15 15:56:45 +05:30
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:
2024-01-17 23:09:18 +03:00
self.handlemessage(bs.DieMessage())
2023-05-15 15:56:45 +05:30
2024-01-17 23:09:18 +03:00
elif isinstance(msg, bs.FreezeMessage):
2023-05-15 15:56:45 +05:30
if not self.frozen:
self.frozen = True
2024-01-17 23:09:18 +03:00
self.drop_bomb_timer = False
self.drop_bots_timer = False
setattr(self.node, 'velocity',
(0, self.to_target.y, 0))
setattr(self.node, 'extra_acceleration',
(0, 0, 0))
2023-05-15 15:56:45 +05:30
self.node.reflection_scale = [2]
def unfrozen():
self.frozen = False
2024-01-17 23:09:18 +03:00
self.drop_bomb_timer = bs.Timer(1.5,
bs.Call(self._drop_bomb),
repeat=True)
self.drop_bots_timer = bs.Timer(15.0,
bs.Call(self._drop_bots),
repeat=True)
2023-05-15 15:56:45 +05:30
self.node.reflection_scale = [0.25]
2024-01-17 23:09:18 +03:00
bs.timer(3.0, unfrozen)
2023-05-15 15:56:45 +05:30
else:
super().handlemessage(msg)
class UFOSet:
2024-01-17 23:09:18 +03:00
"""A container/controller for one or more bs.SpazBots.
2023-05-15 15:56:45 +05:30
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)
]
2024-01-17 23:09:18 +03:00
self._ufo_spawn_sound = bs.getsound('spawn')
2023-05-15 15:56:45 +05:30
self._ufo_spawning_count = 0
2024-01-17 23:09:18 +03:00
self._ufo_bot_update_timer: bs.Timer | None = None
2023-05-15 15:56:45 +05:30
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 = []
2024-01-17 23:09:18 +03:00
bs.print_exception(
2023-05-15 15:56:45 +05:30
'Error updating bot list: '
+ str(self._ufo_bot_lists[self._ufo_bot_update_list])
)
self._bot_update_list = (
2024-01-17 23:09:18 +03:00
self._ufo_bot_update_list + 1
) % self._ufo_bot_list_count
2023-05-15 15:56:45 +05:30
# Update our list of player points for the bots to use.
player_pts = []
2024-01-17 23:09:18 +03:00
for player in bs.getactivity().players:
assert isinstance(player, bs.Player)
2023-05-15 15:56:45 +05:30
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(
(
2024-01-17 23:09:18 +03:00
bs.Vec3(player.actor.node.position),
bs.Vec3(player.actor.node.velocity),
2023-05-15 15:56:45 +05:30
)
)
except Exception:
2024-01-17 23:09:18 +03:00
bs.print_exception('Error on bot-set _update.')
2023-05-15 15:56:45 +05:30
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."""
2024-01-17 23:09:18 +03:00
self._ufo_bot_update_timer = bs.Timer(
0.05, bs.WeakCall(self._update), repeat=True
2023-05-15 15:56:45 +05:30
)
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."""
2024-01-17 23:09:18 +03:00
from bascenev1lib.actor import spawner
2023-05-15 15:56:45 +05:30
spawner.Spawner(
pt=pos,
spawn_time=spawn_time,
send_spawn_message=False,
2024-01-17 23:09:18 +03:00
spawn_callback=bs.Call(
2023-05-15 15:56:45 +05:30
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()
2024-01-17 23:09:18 +03:00
self._ufo_spawn_sound.play(position=pos)
2023-05-15 15:56:45 +05:30
assert spaz.node
spaz.node.handlemessage('flash')
spaz.node.is_area_of_interest = False
2024-01-17 23:09:18 +03:00
spaz.handlemessage(bs.StandMessage(pos, random.uniform(0, 360)))
2023-05-15 15:56:45 +05:30
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:
2024-01-17 23:09:18 +03:00
"""Add a bs.SpazBot instance to the set."""
2023-05-15 15:56:45 +05:30
self._ufo_bot_lists[self._ufo_bot_add_list].append(bot)
self._ufo_bot_add_list = (
2024-01-17 23:09:18 +03:00
self._ufo_bot_add_list + 1) % self._ufo_bot_list_count
2023-05-15 15:56:45 +05:30
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.
2024-01-17 23:09:18 +03:00
activity = bs.getactivity(doraise=False)
2023-05-15 15:56:45 +05:30
if activity is None or activity.expired:
return
for i, bot_list in enumerate(self._ufo_bot_lists):
for bot in bot_list:
2024-01-17 23:09:18 +03:00
bot.handlemessage(bs.DieMessage(immediate=True))
2023-05-15 15:56:45 +05:30
self._ufo_bot_lists[i] = []
2024-01-17 23:09:18 +03:00
class Player(bs.Player['Team']):
2023-05-15 15:56:45 +05:30
"""Our player type for this game."""
2024-01-17 23:09:18 +03:00
class Team(bs.Team[Player]):
2023-05-15 15:56:45 +05:30
"""Our team type for this game."""
2024-01-17 23:09:18 +03:00
# ba_meta export bascenev1.GameActivity
class UFOightGame(bs.TeamGameActivity[Player, Team]):
2023-05-15 15:56:45 +05:30
"""
A co-op game where you try to defeat UFO Boss
as fast as possible
"""
name = 'UFO Fight'
description = 'REal Boss Fight?'
2024-01-17 23:09:18 +03:00
scoreconfig = bs.ScoreConfig(
label='Time', scoretype=bs.ScoreType.MILLISECONDS, lower_is_better=True
2023-05-15 15:56:45 +05:30
)
2024-01-17 23:09:18 +03:00
default_music = bs.MusicType.TO_THE_DEATH
2023-05-15 15:56:45 +05:30
@classmethod
2024-01-17 23:09:18 +03:00
def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
2023-05-15 15:56:45 +05:30
# 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
2024-01-17 23:09:18 +03:00
def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
2023-05-15 15:56:45 +05:30
# We currently support Co-Op only.
2024-01-17 23:09:18 +03:00
return issubclass(sessiontype, bs.CoopSession)
2023-05-15 15:56:45 +05:30
# 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)
2024-01-17 23:09:18 +03:00
self._winsound = bs.getsound('score')
2023-05-15 15:56:45 +05:30
self._won = False
self._timer: OnScreenTimer | None = None
self._bots = UFOSet()
self._preset = str(settings['preset'])
2024-01-17 23:09:18 +03:00
self._credit = bs.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'
})
2023-05-15 15:56:45 +05:30
def on_transition_in(self) -> None:
super().on_transition_in()
2024-01-17 23:09:18 +03:00
gnode = bs.getactivity().globalsnode
2023-05-15 15:56:45 +05:30
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()
2024-01-17 23:09:18 +03:00
2023-05-15 15:56:45 +05:30
# In pro mode there's no powerups.
# Make our on-screen timer and start it roughly when our bots appear.
self._timer = OnScreenTimer()
2024-01-17 23:09:18 +03:00
bs.timer(4.0, self._timer.start)
2023-05-15 15:56:45 +05:30
def checker():
if not self._won:
2024-01-17 23:09:18 +03:00
self.timer = bs.Timer(0.1, self._check_if_won, repeat=True)
2023-05-15 15:56:45 +05:30
2024-01-17 23:09:18 +03:00
bs.timer(10, checker)
activity = bs.get_foreground_host_activity()
2023-05-15 15:56:45 +05:30
point = activity.map.get_flag_position(None)
boss_spawn_pos = (point[0], point[1] + 1.5, point[2])
# Spawn some baddies.
2024-01-17 23:09:18 +03:00
bs.timer(
2023-05-15 15:56:45 +05:30
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.
2024-01-17 23:09:18 +03:00
if isinstance(msg, bs.PlayerDiedMessage):
2023-05-15 15:56:45 +05:30
player = msg.getplayer(Player)
self.stats.player_was_killed(player)
2024-01-17 23:09:18 +03:00
bs.timer(0.1, self._checkroundover)
2023-05-15 15:56:45 +05:30
# 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.
2024-01-17 23:09:18 +03:00
bs.pushcall(self._check_if_won)
2023-05-15 15:56:45 +05:30
# 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()
2024-01-17 23:09:18 +03:00
results = bs.GameResults()
2023-05-15 15:56:45 +05:30
# 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:
2024-01-17 23:09:18 +03:00
elapsed_time_ms = int((bs.time() - self._timer.starttime) * 1000.0)
bs.cameraflash()
self._winsound.play()
2023-05-15 15:56:45 +05:30
for team in self.teams:
for player in team.players:
if player.actor:
2024-01-17 23:09:18 +03:00
player.actor.handlemessage(bs.CelebrateMessage())
2023-05-15 15:56:45 +05:30
results.set_team_score(team, elapsed_time_ms)
# Ends the activity.
self.end(results)
# ba_meta export plugin
2024-01-17 23:09:18 +03:00
class MyUFOFightLevel(babase.Plugin):
2023-05-15 15:56:45 +05:30
def on_app_running(self) -> None:
2024-01-17 23:09:18 +03:00
babase.app.classic.add_coop_practice_level(
bs.Level(
2023-05-15 15:56:45 +05:30
name='The UFO Fight',
displayname='${GAME}',
gametype=UFOightGame,
settings={'preset': 'regular'},
preview_texture_name='footballStadiumPreview',
)
)
2024-01-17 23:09:18 +03:00