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
|
|
|
|