diff --git a/src/plugin/boss/barrage.ts b/src/plugin/boss/barrage.ts index df0ea4a..02e6f56 100644 --- a/src/plugin/boss/barrage.ts +++ b/src/plugin/boss/barrage.ts @@ -17,8 +17,11 @@ export abstract class BarrageBoss extends EventEmitter { /** 开始时刻 */ 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 { * 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 { v.doDamage(this.state); } }); + this.lastTime = now; }; /** @@ -230,8 +236,9 @@ export abstract class Projectile { * @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的弹幕应该全部画在同一层,而且渲染前画布不进行矩阵变换 diff --git a/src/plugin/boss/palaceBoss.ts b/src/plugin/boss/palaceBoss.ts new file mode 100644 index 0000000..013f415 --- /dev/null +++ b/src/plugin/boss/palaceBoss.ts @@ -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; + 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 { + 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 {} +} diff --git a/src/plugin/boss/palaceBossProjectile.ts b/src/plugin/boss/palaceBossProjectile.ts new file mode 100644 index 0000000..bb962ce --- /dev/null +++ b/src/plugin/boss/palaceBossProjectile.ts @@ -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 { + damage: number = 10000; + hitbox: Hitbox.Circle = new Hitbox.Circle(0, 0, 8); + + static ball: Map = 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) { + 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); + } +} diff --git a/src/plugin/boss/towerBoss.ts b/src/plugin/boss/towerBoss.ts index d8ba539..13d7c9f 100644 --- a/src/plugin/boss/towerBoss.ts +++ b/src/plugin/boss/towerBoss.ts @@ -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;