HumanBreak/src/plugin/boss/barrage.ts
2024-12-29 16:12:53 +08:00

453 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { RenderItem, RenderItemPosition } from '@/core/render/item';
import { Transform } from '@/core/render/transform';
import { IStateDamageable } from '@/game/state/interface';
import EventEmitter from 'eventemitter3';
import { Ticker } from 'mutate-animate';
interface BarrageBossEvent {
end: [];
start: [];
}
export abstract class BarrageBoss extends EventEmitter<BarrageBossEvent> {
ticker: Ticker = new Ticker();
/** 这个boss的所有弹幕 */
projectiles: Set<Projectile> = new Set();
/** 开始时刻 */
private startTime: number = 0;
/** 当前帧数 */
frame: number = 0;
/** 上一帧的时刻 */
lastTime: number = 0;
/** 这个boss战的主渲染元素所有弹幕都会在此之上渲染 */
abstract readonly main: BossSprite;
/** 这个boss战中勇士的碰撞箱 */
abstract readonly hitbox: Hitbox.HitboxType;
/** 勇士的状态 */
abstract readonly state: IStateDamageable;
/**
* boss的ai战斗开始后每帧执行一次
* @param time 从战斗开始算起至现在经过了多长时间
* @param frame 从战斗开始算起至现在经过了多少帧,即当前是第几帧
* @param dt 本帧距上一帧多长时间,即上一帧持续了多长时间
*/
abstract ai(time: number, frame: number, dt: number): void;
private tick = () => {
const now = Date.now();
const dt = now - this.lastTime;
this.ai(now - this.startTime, this.frame, dt);
this.frame++;
this.projectiles.forEach(v => {
const time = now - v.startTime;
v.time = time;
v.ai(this, time, v.frame, dt);
v.frame++;
if (time > 60_000) {
this.destroyProjectile(v);
}
if (v.isIntersect(this.hitbox)) {
v.doDamage(this.state);
}
});
this.lastTime = now;
};
/**
* 开始这个弹幕战
*/
start() {
if (this.ticker.funcs.has(this.tick)) {
this.ticker.remove(this.tick);
}
this.startTime = Date.now();
this.frame = 0;
this.ticker.add(this.tick);
}
/**
* 结束这个弹幕战
*/
end() {
if (this.ticker.funcs.has(this.tick)) {
this.ticker.remove(this.tick);
}
}
/**
* 摧毁传入的弹幕
*/
destroyProjectile(projectile: Projectile) {
this.projectiles.delete(projectile);
}
/**
* 用于创建一个弹幕的工厂函数
* @param Proj 弹幕类
* @param x 弹幕的横坐标
* @param y 弹幕的纵坐标
*/
createProjectile<T extends Projectile>(
Proj: new (boss: this) => T,
x: number,
y: number
): T {
const projectile = new Proj(this);
projectile.setPosition(x, y);
return projectile;
}
}
export abstract class BossSprite<
T extends BarrageBoss = BarrageBoss
> extends RenderItem {
/** 这个sprite所属的boss */
readonly boss: T;
constructor(type: RenderItemPosition, boss: T) {
super(type, false);
this.boss = boss;
}
/**
* 在内置渲染函数执行前渲染内容返回false会阻止内置渲染函数执行
* @param canvas 渲染至的画布
* @param transform 渲染时的变换矩阵
*/
protected abstract preDraw(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): boolean;
/**
* 在内置渲染函数执行后渲染内容如果preDraw返回false也会执行本函数
* @param canvas 渲染至的画布
* @param transform 渲染时的变换矩阵
*/
protected abstract postDraw(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void;
protected render(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void {
const pre = this.preDraw(canvas, transform);
if (!pre) {
this.postDraw(canvas, transform);
return;
}
this.renderProjectiles(canvas, transform);
this.postDraw(canvas, transform);
}
/**
* 渲染所有弹幕
* @param canvas 渲染至的画布
* @param transform 渲染时的变换矩阵
*/
protected renderProjectiles(
canvas: MotaOffscreenCanvas2D,
transform: Transform
) {
this.boss.projectiles.forEach(v => {
v.render(canvas, transform);
});
}
}
export abstract class Projectile<T extends BarrageBoss = BarrageBoss> {
/** 这个弹幕从属的boss */
boss: T;
/** 这个弹幕的伤害 */
abstract damage: number;
private _x: number = 0;
get x(): number {
return this._x;
}
set x(v: number) {
this._x = v;
this.updateHitbox(v, this._y);
}
private _y: number = 0;
get y(): number {
return this._y;
}
set y(v: number) {
this._y = v;
this.updateHitbox(this._x, v);
}
/** 弹幕的生成时刻 */
startTime: number = Date.now();
/** 弹幕当前帧数 */
frame: number = 0;
/** 当前弹幕持续时长 */
time: number = 0;
/** 这个弹幕的碰撞箱 */
abstract hitbox: Hitbox.HitboxType;
constructor(boss: T) {
this.boss = boss;
boss.projectiles.add(this);
}
/**
* 判断一个碰撞箱是否与本弹幕的碰撞箱有交叉。
* 此判断应该具有对称性如果用A检测B发生碰撞那么用B检测A也应该发生碰撞。
* @param hitbox 要检测的碰撞箱
*/
abstract isIntersect(hitbox: Hitbox.HitboxType): boolean;
/**
* 当弹幕的横纵坐标改变时,更新碰撞箱
* @param x 弹幕的横坐标
* @param y 弹幕的纵坐标
*/
abstract updateHitbox(x: number, y: number): void;
/**
* 对一个目标造成伤害
* @param target 伤害目标
* @returns 是否成功对目标造成伤害
*/
abstract doDamage(target: IStateDamageable): boolean;
/**
* 设置这个弹幕的位置
*/
setPosition(x: number, y: number) {
this.x = x;
this.y = y;
this.updateHitbox(x, y);
}
/**
* 这个弹幕的ai每帧执行一次直至被销毁在1分钟后会强制被摧毁
* @param boss 从属的boss
* @param time 从弹幕生成开始算起至现在经过了多长时间
* @param frame 从弹幕生成开始算起至现在经过了多少帧,即当前是第几帧
* @param dt 本帧距上一帧多长时间,即上一帧持续了多长时间
*/
abstract ai(boss: T, time: number, frame: number, dt: number): void;
/**
* 这个弹幕的渲染函数原则上一个boss的弹幕应该全部画在同一层而且渲染前画布不进行矩阵变换
* @param canvas 渲染至的画布
* @param transform 渲染时的变换矩阵
*/
abstract render(canvas: MotaOffscreenCanvas2D, transform: Transform): void;
/**
* 摧毁这个弹幕
*/
destroy() {
this.boss.destroyProjectile(this);
}
}
export namespace Hitbox {
export type HitboxType = Line | Rect | Circle;
export class Line {
constructor(
public x1: number,
public y1: number,
public x2: number,
public y2: number
) {}
setPoint1(x: number, y: number) {
this.x1 = x;
this.y1 = y;
}
setPoint2(x: number, y: number) {
this.x2 = x;
this.y2 = y;
}
}
export class Circle {
constructor(
public x: number,
public y: number,
public radius: number
) {}
setRadius(radius: number) {
this.radius = radius;
}
setCenter(x: number, y: number) {
this.x = x;
this.y = y;
}
}
export class Rect {
constructor(
public x: number,
public y: number,
public w: number,
public h: number
) {}
setPosition(x: number, y: number) {
this.x = x;
this.y = y;
}
setSize(w: number, h: number) {
this.w = w;
this.h = h;
}
}
function cross(
x1: number,
y1: number,
x2: number,
y2: number,
x3: number,
y3: number
): number {
const dx1 = x2 - x1;
const dy1 = y2 - y1;
const dx2 = x3 - x1;
const dy2 = y3 - y1;
return dx1 * dy2 - dx2 * dy1;
}
/**
* 检查两条线段是否有交叉
*/
export function checkLineLine(line1: Line, line2: Line) {
const { x1, y1, x2, y2 } = line1;
const { x1: x3, y1: y3, x2: x4, y2: y4 } = line2;
if (
Math.max(x1, x2) < Math.min(x3, x4) ||
Math.min(x1, x2) > Math.max(x3, x4) ||
Math.max(y1, y2) < Math.min(y3, y4) ||
Math.min(y1, y2) > Math.max(y3, y4)
) {
return false;
}
const d1 = cross(x1, y1, x2, y2, x3, y3);
const d2 = cross(x1, y1, x2, y2, x4, y4);
const d3 = cross(x3, y3, x4, y4, x1, y1);
const d4 = cross(x3, y3, x4, y4, x2, y2);
return d1 * d2 < 0 && d3 * d4 < 0;
}
/**
* 检查线段和圆是否有交叉
*/
export function checkLineCircle(line: Line, circle: Circle) {
const { x1, y1, x2, y2 } = line;
const { x: cx, y: cy, radius: r } = circle;
const minX = Math.min(x1, x2);
const maxX = Math.max(x1, x2);
const minY = Math.min(y1, y2);
const maxY = Math.max(y1, y2);
// 检查圆心是否在扩展后的矩形范围之外
if (cx + r < minX || cx - r > maxX || cy + r < minY || cy - r > maxY) {
return false; // 完全不相交
}
// 计算线段的方向向量
const dx = x2 - x1;
const dy = y2 - y1;
// A, B, C 对应二次方程的系数
const a = dx * dx + dy * dy;
const b = 2 * (dx * (x1 - cx) + dy * (y1 - cy));
const c = (x1 - cx) * (x1 - cx) + (y1 - cy) * (y1 - cy) - r * r;
// 计算判别式 Δ
const discriminant = b ** 2 - 4 * a * c;
// 如果判别式小于0则没有交点
if (discriminant < 0) {
return false;
}
// 计算t的解参数化线段的参数
const sqrtDiscriminant = Math.sqrt(discriminant);
const t1 = (-b - sqrtDiscriminant) / (2 * a);
const t2 = (-b + sqrtDiscriminant) / (2 * a);
// 检查 t1 和 t2 是否在 [0, 1] 之间
if ((t1 >= 0 && t1 <= 1) || (t2 >= 0 && t2 <= 1)) {
return true;
}
// 否则没有交点在线段上
return false;
}
/**
* 检查线段与矩形是否有交叉
*/
export function checkLineRect(line: Line, rect: Rect) {
const { x, y, w, h } = rect;
return (
checkLineLine(line, new Line(x, y, x + w, y + h)) ||
checkLineLine(line, new Line(x + w, y, x, y + h))
);
}
/**
* 检查两个圆是否有交叉
*/
export function checkCircleCircle(circle1: Circle, circle2: Circle) {
const dx = circle1.x - circle2.x;
const dy = circle1.y - circle2.y;
const dis = dx ** 2 + dy ** 2;
return dis <= (circle1.radius + circle2.radius) ** 2;
}
/**
* 检查圆与矩形是否有交叉
*/
export function checkCircleRect(circle: Circle, rect: Rect) {
const { x: cx, y: cy, radius: r } = circle;
const { x, y, w, h } = rect;
if (cx > x && cx < x + w && cy > y && cy < y + h) return true;
// 找到圆心到矩形的最近点
const closestX = Math.max(x, Math.min(cx, x + w));
const closestY = Math.max(y, Math.min(cy, y + h));
return Math.hypot(closestX - cx, closestY - cy) <= r;
}
/**
* 检查两个矩形是否有交叉
*/
export function checkRectRect(rect1: Rect, rect2: Rect) {
const { x: x1, y: y1, w: w1, h: h1 } = rect1;
const { x: x3, y: y3, w: w2, h: h2 } = rect2;
const x2 = x1 + w1;
const y2 = y1 + h1;
const x4 = x3 + w2;
const y4 = y3 + h2;
return x2 >= x3 && x4 >= x1 && y2 >= y3 && y4 >= y1;
}
}