mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-19 04:19:30 +08:00
feat: 第二章特殊战第一种弹幕
This commit is contained in:
parent
44276183bb
commit
4102893916
@ -17,8 +17,11 @@ export abstract class BarrageBoss extends EventEmitter<BarrageBossEvent> {
|
|||||||
|
|
||||||
/** 开始时刻 */
|
/** 开始时刻 */
|
||||||
private startTime: number = 0;
|
private startTime: number = 0;
|
||||||
|
|
||||||
/** 当前帧数 */
|
/** 当前帧数 */
|
||||||
frame: number = 0;
|
frame: number = 0;
|
||||||
|
/** 上一帧的时刻 */
|
||||||
|
lastTime: number = 0;
|
||||||
|
|
||||||
/** 这个boss战的主渲染元素,所有弹幕都会在此之上渲染 */
|
/** 这个boss战的主渲染元素,所有弹幕都会在此之上渲染 */
|
||||||
abstract readonly main: BossSprite;
|
abstract readonly main: BossSprite;
|
||||||
@ -31,17 +34,19 @@ export abstract class BarrageBoss extends EventEmitter<BarrageBossEvent> {
|
|||||||
* boss的ai,战斗开始后,每帧执行一次
|
* boss的ai,战斗开始后,每帧执行一次
|
||||||
* @param time 从战斗开始算起至现在经过了多长时间
|
* @param time 从战斗开始算起至现在经过了多长时间
|
||||||
* @param frame 从战斗开始算起至现在经过了多少帧,即当前是第几帧
|
* @param frame 从战斗开始算起至现在经过了多少帧,即当前是第几帧
|
||||||
|
* @param dt 本帧距上一帧多长时间,即上一帧持续了多长时间
|
||||||
*/
|
*/
|
||||||
abstract ai(time: number, frame: number): void;
|
abstract ai(time: number, frame: number, dt: number): void;
|
||||||
|
|
||||||
private tick = () => {
|
private tick = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
this.ai(now - this.startTime, this.frame);
|
const dt = now - this.lastTime;
|
||||||
|
this.ai(now - this.startTime, this.frame, dt);
|
||||||
this.frame++;
|
this.frame++;
|
||||||
this.projectiles.forEach(v => {
|
this.projectiles.forEach(v => {
|
||||||
const time = now - v.startTime;
|
const time = now - v.startTime;
|
||||||
v.time = time;
|
v.time = time;
|
||||||
v.ai(this, time, v.frame);
|
v.ai(this, time, v.frame, dt);
|
||||||
v.frame++;
|
v.frame++;
|
||||||
if (time > 60_000) {
|
if (time > 60_000) {
|
||||||
this.destroyProjectile(v);
|
this.destroyProjectile(v);
|
||||||
@ -50,6 +55,7 @@ export abstract class BarrageBoss extends EventEmitter<BarrageBossEvent> {
|
|||||||
v.doDamage(this.state);
|
v.doDamage(this.state);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.lastTime = now;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -230,8 +236,9 @@ export abstract class Projectile<T extends BarrageBoss = BarrageBoss> {
|
|||||||
* @param boss 从属的boss
|
* @param boss 从属的boss
|
||||||
* @param time 从弹幕生成开始算起至现在经过了多长时间
|
* @param time 从弹幕生成开始算起至现在经过了多长时间
|
||||||
* @param frame 从弹幕生成开始算起至现在经过了多少帧,即当前是第几帧
|
* @param frame 从弹幕生成开始算起至现在经过了多少帧,即当前是第几帧
|
||||||
|
* @param dt 本帧距上一帧多长时间,即上一帧持续了多长时间
|
||||||
*/
|
*/
|
||||||
abstract ai(boss: T, time: number, frame: number): void;
|
abstract ai(boss: T, time: number, frame: number, dt: number): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 这个弹幕的渲染函数,原则上一个boss的弹幕应该全部画在同一层,而且渲染前画布不进行矩阵变换
|
* 这个弹幕的渲染函数,原则上一个boss的弹幕应该全部画在同一层,而且渲染前画布不进行矩阵变换
|
||||||
|
131
src/plugin/boss/palaceBoss.ts
Normal file
131
src/plugin/boss/palaceBoss.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { IStateDamageable } from '@/game/state/interface';
|
||||||
|
import { BarrageBoss, BossSprite, Hitbox } from './barrage';
|
||||||
|
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
HeroRenderer,
|
||||||
|
LayerGroup,
|
||||||
|
MotaRenderer,
|
||||||
|
RenderItem,
|
||||||
|
Shader,
|
||||||
|
Transform
|
||||||
|
} from '@/core/render';
|
||||||
|
import { Pop } from '../fx/pop';
|
||||||
|
import { SplittableBall } from './palaceBossProjectile';
|
||||||
|
import { PointEffect } from '../fx/pointShader';
|
||||||
|
|
||||||
|
Mota.require('var', 'loading').once('coreInit', () => {
|
||||||
|
const shader = new Shader();
|
||||||
|
shader.size(480, 480);
|
||||||
|
shader.setHD(true);
|
||||||
|
shader.setZIndex(120);
|
||||||
|
PalaceBoss.shader = shader;
|
||||||
|
PalaceBoss.effect.create(shader, 40);
|
||||||
|
});
|
||||||
|
|
||||||
|
const enum BossStage {
|
||||||
|
Prologue,
|
||||||
|
|
||||||
|
Stage1,
|
||||||
|
Stage2,
|
||||||
|
Stage3,
|
||||||
|
Stage4,
|
||||||
|
|
||||||
|
End
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PalaceBoss extends BarrageBoss {
|
||||||
|
static effect: PointEffect = new PointEffect();
|
||||||
|
static shader: Shader;
|
||||||
|
|
||||||
|
main: BossSprite<BarrageBoss>;
|
||||||
|
hitbox: Hitbox.Circle;
|
||||||
|
state: IStateDamageable;
|
||||||
|
|
||||||
|
private stage: BossStage = BossStage.Prologue;
|
||||||
|
|
||||||
|
/** 用于展示傅里叶频谱的背景元素 */
|
||||||
|
private back: SonicBack;
|
||||||
|
/** 楼层渲染元素 */
|
||||||
|
private group: LayerGroup;
|
||||||
|
/** 楼层渲染容器 */
|
||||||
|
private mapDraw: Container;
|
||||||
|
/** 伤害弹出 */
|
||||||
|
pop: Pop;
|
||||||
|
|
||||||
|
private heroHp: number = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const render = MotaRenderer.get('render-main')!;
|
||||||
|
this.group = render.getElementById('layer-main') as LayerGroup;
|
||||||
|
this.mapDraw = render.getElementById('map-draw') as Container;
|
||||||
|
this.pop = render.getElementById('pop-main') as Pop;
|
||||||
|
|
||||||
|
this.state = core.status.hero;
|
||||||
|
this.main = new BossEffect('static', this);
|
||||||
|
this.back = new SonicBack('static');
|
||||||
|
const { x, y } = core.status.hero.loc;
|
||||||
|
const cell = 32;
|
||||||
|
this.hitbox = new Hitbox.Circle(x + cell / 2, y + cell / 2, cell / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
override start(): void {
|
||||||
|
super.start();
|
||||||
|
|
||||||
|
PalaceBoss.shader.append(this.mapDraw);
|
||||||
|
this.main.append(this.group);
|
||||||
|
|
||||||
|
// const event = this.group.getLayer('event');
|
||||||
|
// const hero = event?.getExtends('floor-hero') as HeroRenderer;
|
||||||
|
// hero?.on('moveTick', this.moveTick);
|
||||||
|
|
||||||
|
SplittableBall.init({});
|
||||||
|
this.heroHp = core.status.hero.hp;
|
||||||
|
}
|
||||||
|
|
||||||
|
override end(): void {
|
||||||
|
super.end();
|
||||||
|
|
||||||
|
PalaceBoss.shader.remove();
|
||||||
|
this.main.remove();
|
||||||
|
this.back.remove();
|
||||||
|
this.main.destroy();
|
||||||
|
this.back.destroy();
|
||||||
|
|
||||||
|
// const event = this.group.getLayer('event');
|
||||||
|
// const hero = event?.getExtends('floor-hero') as HeroRenderer;
|
||||||
|
// hero?.off('moveTick', this.moveTick);
|
||||||
|
|
||||||
|
SplittableBall.end();
|
||||||
|
|
||||||
|
PalaceBoss.effect.end();
|
||||||
|
core.status.hero.hp = this.heroHp;
|
||||||
|
|
||||||
|
Mota.Plugin.require('replay_g').clip('choices:0');
|
||||||
|
}
|
||||||
|
|
||||||
|
ai(time: number, frame: number): void {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BossEffect extends BossSprite<PalaceBoss> {
|
||||||
|
protected preDraw(
|
||||||
|
canvas: MotaOffscreenCanvas2D,
|
||||||
|
transform: Transform
|
||||||
|
): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected postDraw(
|
||||||
|
canvas: MotaOffscreenCanvas2D,
|
||||||
|
transform: Transform
|
||||||
|
): void {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SonicBack extends RenderItem {
|
||||||
|
protected render(
|
||||||
|
canvas: MotaOffscreenCanvas2D,
|
||||||
|
transform: Transform
|
||||||
|
): void {}
|
||||||
|
}
|
261
src/plugin/boss/palaceBossProjectile.ts
Normal file
261
src/plugin/boss/palaceBossProjectile.ts
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||||
|
import { Transform } from '@/core/render';
|
||||||
|
import { IStateDamageable } from '@/game/state/interface';
|
||||||
|
import { Hitbox, Projectile } from './barrage';
|
||||||
|
import type { PalaceBoss } from './palaceBoss';
|
||||||
|
import { clamp } from '../utils';
|
||||||
|
|
||||||
|
function popDamage(damage: number, boss: PalaceBoss, color: string) {
|
||||||
|
const { x, y } = core.status.hero.loc;
|
||||||
|
boss.pop.addPop(
|
||||||
|
(-damage).toString(),
|
||||||
|
1000,
|
||||||
|
x * 32 + 16,
|
||||||
|
y * 32 + 16,
|
||||||
|
color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISplitData {
|
||||||
|
split: boolean;
|
||||||
|
/** 分裂时刻,以弹幕被创建时刻为基准 */
|
||||||
|
time: number;
|
||||||
|
/** 分裂起始角度,以该弹幕朝向方向为 0 */
|
||||||
|
startAngle: number;
|
||||||
|
/** 分裂终止角度,以该弹幕朝向方向为 0 */
|
||||||
|
endAngle: number;
|
||||||
|
/** 每秒加速度 */
|
||||||
|
acc: number;
|
||||||
|
/** 初始速度 */
|
||||||
|
startVel: number;
|
||||||
|
/** 终止速度 */
|
||||||
|
endVel: number;
|
||||||
|
/** 持续时长 */
|
||||||
|
lastTime: number;
|
||||||
|
/** 分裂数量 */
|
||||||
|
count: number;
|
||||||
|
/** 这个弹幕分裂产生的弹幕的分裂信息,不填则表示产生的弹幕不会分裂 */
|
||||||
|
data?: ISplitData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SplittableBall extends Projectile<PalaceBoss> {
|
||||||
|
damage: number = 10000;
|
||||||
|
hitbox: Hitbox.Circle = new Hitbox.Circle(0, 0, 8);
|
||||||
|
|
||||||
|
static ball: Map<string, MotaOffscreenCanvas2D> = new Map();
|
||||||
|
|
||||||
|
private damaged: boolean = false;
|
||||||
|
private splitData?: ISplitData;
|
||||||
|
private last: number = 60_000;
|
||||||
|
|
||||||
|
/** 角度,水平向右为 0,顺时针旋转一圈为 Math.PI * 2 */
|
||||||
|
private angle: number = 0;
|
||||||
|
/** 每秒加速度 */
|
||||||
|
private acc: number = 0;
|
||||||
|
/** 初始速度,每秒多少像素 */
|
||||||
|
private startVel: number = 0;
|
||||||
|
/** 终止速度 */
|
||||||
|
private endVel: number = 0;
|
||||||
|
/** 弹幕颜色 */
|
||||||
|
private color?: string;
|
||||||
|
|
||||||
|
private startVelX: number = 0;
|
||||||
|
private startVelY: number = 0;
|
||||||
|
private endVelX: number = 0;
|
||||||
|
private endVelY: number = 0;
|
||||||
|
private vx: number = 0;
|
||||||
|
private vy: number = 0;
|
||||||
|
// 加速度
|
||||||
|
private ax: number = 0;
|
||||||
|
private ay: number = 0;
|
||||||
|
|
||||||
|
static init(colors: Record<string, string[]>) {
|
||||||
|
this.ball.clear();
|
||||||
|
for (const [key, color] of Object.entries(colors)) {
|
||||||
|
const canvas = new MotaOffscreenCanvas2D();
|
||||||
|
canvas.size(32, 32);
|
||||||
|
canvas.withGameScale(true);
|
||||||
|
canvas.setHD(true);
|
||||||
|
const ctx = canvas.ctx;
|
||||||
|
const gradient = ctx.createRadialGradient(16, 16, 8, 16, 16, 16);
|
||||||
|
const step = 1 / (color.length - 1);
|
||||||
|
for (let i = 0; i < color.length; i++) {
|
||||||
|
gradient.addColorStop(i * step, color[i]);
|
||||||
|
}
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.arc(16, 16, 16, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
canvas.freeze();
|
||||||
|
this.ball.set(key, canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static end() {
|
||||||
|
this.ball.forEach(v => {
|
||||||
|
v.clear();
|
||||||
|
v.delete();
|
||||||
|
});
|
||||||
|
this.ball.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置持续时长
|
||||||
|
* @param time 持续时长
|
||||||
|
*/
|
||||||
|
setLastTime(time: number) {
|
||||||
|
this.last = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置这个弹幕的分裂数据
|
||||||
|
* @param data 分裂数据,不填表示该弹幕不会分裂
|
||||||
|
*/
|
||||||
|
setSplitData(data?: ISplitData) {
|
||||||
|
this.splitData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算速度分量信息
|
||||||
|
*/
|
||||||
|
private calVel() {
|
||||||
|
const sin = Math.sin(this.angle);
|
||||||
|
const cos = Math.cos(this.angle);
|
||||||
|
const vel = Math.hypot(this.vx, this.vy);
|
||||||
|
|
||||||
|
this.startVelX = this.startVel * cos;
|
||||||
|
this.startVelY = this.startVel * sin;
|
||||||
|
this.endVelX = this.endVel * cos;
|
||||||
|
this.endVelY = this.endVel * sin;
|
||||||
|
this.ax = this.acc * cos;
|
||||||
|
this.ay = this.acc * sin;
|
||||||
|
this.vx = vel * cos;
|
||||||
|
this.vy = vel * sin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置弹幕速度朝向
|
||||||
|
* @param angle 朝向
|
||||||
|
*/
|
||||||
|
setAngle(angle: number) {
|
||||||
|
this.angle = angle;
|
||||||
|
this.calVel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置速度
|
||||||
|
* @param start 起始速度
|
||||||
|
* @param end 终止速度
|
||||||
|
*/
|
||||||
|
setVel(start: number, end: number) {
|
||||||
|
this.startVel = start;
|
||||||
|
this.endVel = end;
|
||||||
|
this.calVel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置加速度
|
||||||
|
* @param acc 加速度,每秒加速多少像素
|
||||||
|
*/
|
||||||
|
setAcc(acc: number) {
|
||||||
|
this.acc = acc;
|
||||||
|
this.calVel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置弹幕的颜色
|
||||||
|
* @param color 颜色
|
||||||
|
*/
|
||||||
|
setColor(color: string) {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
isIntersect(hitbox: Hitbox.HitboxType): boolean {
|
||||||
|
if (hitbox instanceof Hitbox.Circle) {
|
||||||
|
return Hitbox.checkCircleCircle(hitbox, this.hitbox);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHitbox(x: number, y: number): void {
|
||||||
|
this.hitbox.setCenter(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
doDamage(target: IStateDamageable): boolean {
|
||||||
|
if (this.damaged) return false;
|
||||||
|
target.hp -= this.damage;
|
||||||
|
this.damaged = true;
|
||||||
|
core.drawHeroAnimate('hand');
|
||||||
|
popDamage(this.damage, this.boss, '#ff8180');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private split(boss: PalaceBoss) {
|
||||||
|
if (!this.splitData?.split) return;
|
||||||
|
const {
|
||||||
|
startAngle,
|
||||||
|
endAngle,
|
||||||
|
startVel,
|
||||||
|
endVel,
|
||||||
|
acc,
|
||||||
|
lastTime,
|
||||||
|
count,
|
||||||
|
data
|
||||||
|
} = this.splitData;
|
||||||
|
|
||||||
|
const sa = this.angle + startAngle;
|
||||||
|
const ea = this.angle + endAngle;
|
||||||
|
const step = (ea - sa - 1) / count;
|
||||||
|
const { x, y } = this.hitbox;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const proj = boss.createProjectile(SplittableBall, x, y);
|
||||||
|
proj.setAngle(sa + step * i);
|
||||||
|
proj.setAcc(acc);
|
||||||
|
proj.setVel(startVel, endVel);
|
||||||
|
proj.setLastTime(lastTime);
|
||||||
|
proj.setSplitData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ai(boss: PalaceBoss, time: number, frame: number, dt: number): void {
|
||||||
|
if (this.splitData?.split) {
|
||||||
|
if (time > this.splitData.time) {
|
||||||
|
this.split(boss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (time > this.last) {
|
||||||
|
this.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const p = dt / 1000;
|
||||||
|
this.vx += this.ax * p;
|
||||||
|
this.vy += this.ay * p;
|
||||||
|
|
||||||
|
const sx = Math.sign(this.vx);
|
||||||
|
const sy = Math.sign(this.vy);
|
||||||
|
const cx = clamp(
|
||||||
|
Math.abs(this.vx),
|
||||||
|
Math.abs(this.startVelX),
|
||||||
|
Math.abs(this.endVelX)
|
||||||
|
);
|
||||||
|
const cy = clamp(
|
||||||
|
Math.abs(this.vy),
|
||||||
|
Math.abs(this.startVelY),
|
||||||
|
Math.abs(this.endVelY)
|
||||||
|
);
|
||||||
|
this.vx = cx * sx;
|
||||||
|
this.vy = cy * sy;
|
||||||
|
|
||||||
|
const { x, y } = this.hitbox;
|
||||||
|
this.setPosition(x + this.vx * p, y + this.vy * p);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(canvas: MotaOffscreenCanvas2D, transform: Transform): void {
|
||||||
|
if (!this.color) return;
|
||||||
|
const texture = SplittableBall.ball.get(this.color);
|
||||||
|
if (!texture) return;
|
||||||
|
const ctx = canvas.ctx;
|
||||||
|
ctx.drawImage(texture.canvas, this.x - 16, this.y - 16, 32, 32);
|
||||||
|
}
|
||||||
|
}
|
@ -179,6 +179,8 @@ export class TowerBoss extends BarrageBoss {
|
|||||||
this.healthBar.remove();
|
this.healthBar.remove();
|
||||||
this.word.remove();
|
this.word.remove();
|
||||||
this.main.remove();
|
this.main.remove();
|
||||||
|
this.main.destroy();
|
||||||
|
this.healthBar.destroy();
|
||||||
|
|
||||||
const event = this.group.getLayer('event');
|
const event = this.group.getLayer('event');
|
||||||
const hero = event?.getExtends('floor-hero') as HeroRenderer;
|
const hero = event?.getExtends('floor-hero') as HeroRenderer;
|
||||||
|
Loading…
Reference in New Issue
Block a user