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;
|
||||
|
||||
/** 当前帧数 */
|
||||
frame: number = 0;
|
||||
/** 上一帧的时刻 */
|
||||
lastTime: number = 0;
|
||||
|
||||
/** 这个boss战的主渲染元素,所有弹幕都会在此之上渲染 */
|
||||
abstract readonly main: BossSprite;
|
||||
@ -31,17 +34,19 @@ export abstract class BarrageBoss extends EventEmitter<BarrageBossEvent> {
|
||||
* boss的ai,战斗开始后,每帧执行一次
|
||||
* @param time 从战斗开始算起至现在经过了多长时间
|
||||
* @param frame 从战斗开始算起至现在经过了多少帧,即当前是第几帧
|
||||
* @param dt 本帧距上一帧多长时间,即上一帧持续了多长时间
|
||||
*/
|
||||
abstract ai(time: number, frame: number): void;
|
||||
abstract ai(time: number, frame: number, dt: number): void;
|
||||
|
||||
private tick = () => {
|
||||
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.projectiles.forEach(v => {
|
||||
const time = now - v.startTime;
|
||||
v.time = time;
|
||||
v.ai(this, time, v.frame);
|
||||
v.ai(this, time, v.frame, dt);
|
||||
v.frame++;
|
||||
if (time > 60_000) {
|
||||
this.destroyProjectile(v);
|
||||
@ -50,6 +55,7 @@ export abstract class BarrageBoss extends EventEmitter<BarrageBossEvent> {
|
||||
v.doDamage(this.state);
|
||||
}
|
||||
});
|
||||
this.lastTime = now;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -230,8 +236,9 @@ export abstract class Projectile<T extends BarrageBoss = BarrageBoss> {
|
||||
* @param boss 从属的boss
|
||||
* @param time 从弹幕生成开始算起至现在经过了多长时间
|
||||
* @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的弹幕应该全部画在同一层,而且渲染前画布不进行矩阵变换
|
||||
|
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.word.remove();
|
||||
this.main.remove();
|
||||
this.main.destroy();
|
||||
this.healthBar.destroy();
|
||||
|
||||
const event = this.group.getLayer('event');
|
||||
const hero = event?.getExtends('floor-hero') as HeroRenderer;
|
||||
|
Loading…
Reference in New Issue
Block a user