mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-19 17:16:08 +08:00
453 lines
12 KiB
TypeScript
453 lines
12 KiB
TypeScript
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;
|
||
}
|
||
}
|