diff --git a/public/project/floors/tower7.js b/public/project/floors/tower7.js index 40b668e..86f5bd2 100644 --- a/public/project/floors/tower7.js +++ b/public/project/floors/tower7.js @@ -192,7 +192,21 @@ main.floors.tower7= [527,527,527,527,527,527,527,527,527,527,527,527,527,527,527] ], "bgmap": [ - + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526], + [526,526,526,526,526,526,526,526,526,526,526,526,526,526,526] ], "fgmap": [ diff --git a/src/game/state/interface.ts b/src/game/state/interface.ts new file mode 100644 index 0000000..2c9b741 --- /dev/null +++ b/src/game/state/interface.ts @@ -0,0 +1,4 @@ +export interface IStateDamageable { + /** 生命值 */ + hp: number; +} diff --git a/src/plugin/boss/barrage.ts b/src/plugin/boss/barrage.ts index 1ffcfc5..e520ea3 100644 --- a/src/plugin/boss/barrage.ts +++ b/src/plugin/boss/barrage.ts @@ -1,3 +1,7 @@ +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 { Ticker } from 'mutate-animate'; export abstract class BarrageBoss { @@ -8,7 +12,14 @@ export abstract class BarrageBoss { /** 开始时刻 */ private startTime: number = 0; /** 当前帧数 */ - private frame: number = 0; + frame: number = 0; + + /** 这个boss战的主渲染元素,所有弹幕都会在此之上渲染 */ + abstract readonly main: BossSprite; + /** 这个boss战中勇士的碰撞箱 */ + abstract readonly hitbox: Hitbox.HitboxType; + /** 勇士的状态 */ + abstract readonly state: IStateDamageable; /** * boss的ai,战斗开始后,每帧执行一次 @@ -19,9 +30,19 @@ export abstract class BarrageBoss { private tick = () => { const now = Date.now(); - this.ai(now - this.startTime, this.frame++); + this.ai(now - this.startTime, this.frame); + this.frame++; this.projectiles.forEach(v => { - v.ai(this, now - v.startTime, v.frame++); + const time = now - v.startTime; + v.time = time; + v.ai(this, time, v.frame); + v.frame++; + if (time > 60_000) { + this.destroyProjectile(v); + } + if (v.isIntersect(this.hitbox)) { + v.doDamage(this.state); + } }); }; @@ -59,48 +80,159 @@ export abstract class BarrageBoss { * @param x 弹幕的横坐标 * @param y 弹幕的纵坐标 */ - createProjectile( - Proj: new (boss: BarrageBoss) => Projectile, + createProjectile( + Proj: new (boss: this) => T, x: number, y: number - ) { + ): T { const projectile = new Proj(this); projectile.setPosition(x, y); return projectile; } } -export abstract class 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 { /** 这个弹幕从属的boss */ - boss: BarrageBoss; - x: number = 0; - y: number = 0; + 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; - constructor(boss: BarrageBoss) { + /** 这个弹幕的碰撞箱 */ + 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,每帧执行一次,直至被销毁 + * 这个弹幕的ai,每帧执行一次,直至被销毁,在1分钟后会强制被摧毁 * @param boss 从属的boss * @param time 从弹幕生成开始算起至现在经过了多长时间 * @param frame 从弹幕生成开始算起至现在经过了多少帧,即当前是第几帧 */ - abstract ai(boss: BarrageBoss, time: number, frame: number): void; + abstract ai(boss: T, time: number, frame: number): void; + + /** + * 这个弹幕的渲染函数,原则上一个boss的弹幕应该全部画在同一层,而且渲染前画布不进行矩阵变换 + * @param canvas 渲染至的画布 + * @param transform 渲染时的变换矩阵 + */ + abstract render(canvas: MotaOffscreenCanvas2D, transform: Transform): void; /** * 摧毁这个弹幕 @@ -111,18 +243,15 @@ export abstract class Projectile { } export namespace Hitbox { - export class Line { - x1: number; - y1: number; - x2: number; - y2: number; + export type HitboxType = Line | Rect | Circle; - constructor(x1: number, y1: number, x2: number, y2: number) { - this.x1 = x1; - this.x2 = x2; - this.y1 = y1; - this.y2 = y2; - } + export class Line { + constructor( + public x1: number, + public y1: number, + public x2: number, + public y2: number + ) {} setPoint1(x: number, y: number) { this.x1 = x; @@ -136,15 +265,11 @@ export namespace Hitbox { } export class Circle { - x: number; - y: number; - radius: number; - - constructor(x: number, y: number, radius: number) { - this.x = x; - this.y = y; - this.radius = radius; - } + constructor( + public x: number, + public y: number, + public radius: number + ) {} setRadius(radius: number) { this.radius = radius; @@ -157,17 +282,12 @@ export namespace Hitbox { } export class Rect { - x: number; - y: number; - w: number; - h: number; - - constructor(x: number, y: number, w: number, h: number) { - this.x = x; - this.y = y; - this.w = w; - this.h = h; - } + constructor( + public x: number, + public y: number, + public w: number, + public h: number + ) {} setPosition(x: number, y: number) { this.x = x; diff --git a/src/plugin/boss/towerBoss.ts b/src/plugin/boss/towerBoss.ts index f0e7ab3..6779f57 100644 --- a/src/plugin/boss/towerBoss.ts +++ b/src/plugin/boss/towerBoss.ts @@ -1,12 +1,34 @@ import { Shader } from '@/core/render/shader'; import { PointEffect } from '../fx/pointShader'; -import { BarrageBoss } from './barrage'; +import { BarrageBoss, BossSprite, Hitbox } from './barrage'; import { MotaRenderer } from '@/core/render/render'; import { LayerGroup } from '@/core/render/preset/layer'; import { RenderItem } from '@/core/render/item'; import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; import { Transform } from '@/core/render/transform'; -import { Animation, hyper, power, sleep, Transition } from 'mutate-animate'; +import { + Animation, + hyper, + power, + sleep, + TimingFn, + Transition +} from 'mutate-animate'; +import { Container } from '@/core/render/container'; +import { + ArrowProjectile, + PortalProjectile, + ProjectileDirection +} from './towerBossProjectile'; +import { IStateDamageable } from '@/game/state/interface'; + +Mota.require('var', 'loading').once('coreInit', () => { + const shader = new Shader(); + shader.size(480, 480); + shader.setHD(true); + TowerBoss.shader = shader; + TowerBoss.effect.create(shader, 40); +}); const enum TowerBossStage { /** 开场白阶段 */ @@ -19,6 +41,8 @@ const enum TowerBossStage { Stage3, Dialogue3, Stage4, + Stage5, + Stage6, End } @@ -29,22 +53,40 @@ const enum HealthBarStatus { End } -Mota.require('var', 'loading').once('coreInit', () => { - const shader = new Shader(); - shader.size(480, 480); - shader.setHD(true); - TowerBoss.shader = shader; - TowerBoss.effect.create(shader, 40); -}); +interface TowerBossAttack { + x: number; + y: number; + damage: number; + /** 生成时刻 */ + spwan: number; + /** 持续时长 */ + last: number; +} -class TowerBoss extends BarrageBoss { +interface AttackCircleRenderable { + cx: number; + cy: number; + alpha: number; + lineOffset: number; +} + +export class TowerBoss extends BarrageBoss { static effect: PointEffect = new PointEffect(); static shader: Shader; /** boss战阶段 */ stage: TowerBossStage = TowerBossStage.Prologue; + /** 当前boss血量 */ + hp: number = 10000; + /** 当前时刻 */ + time: number = 0; - private hp: number = 10000; + readonly hitbox: Hitbox.Rect; + readonly state: IStateDamageable; + readonly main: BossEffect; + + /** 攻击位点 */ + private attackLoc: Set = new Set(); /** 血条显示元素 */ private healthBar: HealthBar; @@ -52,20 +94,398 @@ class TowerBoss extends BarrageBoss { private word: Word; /** 楼层渲染元素 */ private group: LayerGroup; + /** 楼层渲染容器 */ + private mapDraw: Container; + + /** 每个阶段的进度,具体定义参考 ai 函数开头 */ + private stageProgress: number = 0; + /** 当前阶段的开始时刻 */ + private stageStartTime: number = 0; + /** 每一阶段的攻击boss次数 */ + private attackTime: number = 0; + /** 攻击boss的红圈间隔时长 */ + private attackInterval: number = 7000; + private attackIn: TimingFn = hyper('sin', 'out'); + private attackOut: TimingFn = hyper('sin', 'in'); + + /** 使用技能1 智慧之矢 的次数 */ + private skill1Time: number = 0; + /** 使用技能2 随机传送 的次数 */ + private skill2Time: number = 0; + /** 使用技能3 冰锥 的次数 */ + private skill3Time: number = 0; + /** 技能1的释放间隔 */ + private skill1Interval: number = 10000; + /** 技能2的释放间隔 */ + private skill2Interval: number = 7000; + /** 技能3的释放间隔 */ + private skill3Interval: number = 13000; + + /** 使用技能4 随机闪电 的次数 */ + private skill4Time: number = 0; + /** 使用技能5 球形闪电 的次数 */ + private skill5Time: number = 0; + /** 技能4的释放间隔 */ + private skill4Interval: number = 4000; + /** 技能5的释放间隔 */ + private skill5Interval: number = 12000; + + /** 使用技能6 炸弹 的次数 */ + private skill6Time: number = 0; + /** 使用技能7 连锁闪电 的次数 */ + private skill7Time: number = 0; + /** 技能6的释放间隔 */ + private skill6Interval: number = 500; + /** 技能7的释放间隔 */ + private skill7Interval: number = 10000; constructor() { super(); this.healthBar = new HealthBar('absolute'); this.word = new Word('absolute'); + this.main = new BossEffect('absolute', this); const render = MotaRenderer.get('render-main')!; this.group = render.getElementById('layer-main') as LayerGroup; + this.mapDraw = render.getElementById('map-draw') as Container; this.healthBar.init(); this.word.init(); + this.main.init(); + + this.healthBar.append(this.group); + this.word.append(this.group); + this.main.append(this.group); + + const { x, y } = core.status.hero.loc; + const cell = 32; + this.hitbox = new Hitbox.Rect(x * cell + 4, y * cell + 16, 24, 32); + this.state = core.status.hero; } - ai(time: number, frame: number): void {} + override start() { + super.start(); + this.group.remove(); + this.group.append(TowerBoss.shader); + TowerBoss.shader.append(this.mapDraw); + + ArrowProjectile.init(); + PortalProjectile.init(); + } + + override end() { + super.end(); + TowerBoss.shader.remove(); + this.group.append(this.mapDraw); + this.healthBar.remove(); + this.word.remove(); + this.main.remove(); + + ArrowProjectile.end(); + PortalProjectile.end(); + } + + /** + * 用于全局检测,例如受伤、攻击boss等 + */ + check(time: number) { + this.checkLose(); + } + + private checkLose() { + if (core.status.hero.hp < 0) { + core.lose(); + core.updateStatusBar(); + this.end(); + } + } + + /** + * 攻击boss + * @param damage 造成的伤害 + */ + attackBoss(damage: number) { + this.hp -= damage; + this.healthBar.set(this.hp); + // 先用drawAnimate凑活一下,等下个版本提供更好的 api + if (this.stage === TowerBossStage.Stage4) { + core.drawAnimate('hand', 7, 2); + } else if (this.stage === TowerBossStage.Stage5) { + core.drawAnimate('hand', 7, 3); + } else if (this.stage === TowerBossStage.Stage6) { + core.drawAnimate('hand', 7, 4); + } else { + core.drawAnimate('hand', 7, 1); + } + } + + /** + * 添加攻击boss的圆圈 + * @param last 持续时长 + * @param damage 造成的伤害 + */ + addAttackCircle(last: number, damage: number) { + let nx = 0; + let ny = 0; + if (this.stage === TowerBossStage.Stage4) { + nx = Math.floor(Math.random() * 11 + 2); + ny = Math.floor(Math.random() * 11 + 2); + } else if (this.stage === TowerBossStage.Stage5) { + nx = Math.floor(Math.random() * 9 + 3); + ny = Math.floor(Math.random() * 9 + 3); + } else if (this.stage === TowerBossStage.Stage6) { + nx = Math.floor(Math.random() * 7 + 4); + ny = Math.floor(Math.random() * 7 + 4); + } else { + nx = Math.floor(Math.random() * 13 + 1); + ny = Math.floor(Math.random() * 13 + 1); + } + const obj: TowerBossAttack = { + x: nx, + y: ny, + spwan: this.time, + damage, + last + }; + this.attackLoc.add(obj); + } + + private getAttackCircleRenderable(): AttackCircleRenderable[] { + return [...this.attackLoc].map(v => { + const progress = (this.time - v.spwan) / v.last; + let alpha = 1; + let offset = 0; + if (progress < 0.1) { + alpha = progress * 10; + offset = 32 * this.attackIn(10 * (0.1 - progress)); + } else if (progress > 0.9) { + alpha = 10 * (1 - progress); + offset = 32 * this.attackOut(10 * (progress - 0.9)); + } + return { + cx: v.x * 32, + cy: v.y * 32, + alpha, + lineOffset: offset + }; + }); + } + + private renderAttack() { + const renderable = this.getAttackCircleRenderable(); + this.main.setAttackCircle(renderable); + } + + ai(time: number, frame: number): void { + this.time = time; + const fixedTime = time - this.stageStartTime; + this.main.update(); + this.renderAttack(); + this.check(time); + switch (this.stage) { + case TowerBossStage.Prologue: + this.aiPrologue(fixedTime, frame); + break; + case TowerBossStage.Stage1: + this.aiStage1(fixedTime, frame); + break; + case TowerBossStage.Dialogue1: + this.aiDialogue1(fixedTime, frame); + break; + case TowerBossStage.Stage2: + this.aiStage2(fixedTime, frame); + break; + case TowerBossStage.Dialogue2: + this.aiDialogue2(fixedTime, frame); + break; + case TowerBossStage.Stage3: + this.aiStage3(fixedTime, frame); + break; + case TowerBossStage.Dialogue3: + this.aiDialogue3(fixedTime, frame); + break; + case TowerBossStage.Stage4: + this.aiStage4(fixedTime, frame); + break; + case TowerBossStage.Stage5: + this.aiStage5(fixedTime, frame); + break; + case TowerBossStage.Stage6: + this.aiStage6(fixedTime, frame); + break; + case TowerBossStage.End: + this.aiEnd(fixedTime, frame); + break; + } + } + + /** + * 切换boss阶段 + * @param stage 切换至的阶段 + * @param time 在当前阶段经过的时间 + */ + private changeStage(stage: TowerBossStage, time: number) { + this.stage = stage; + this.stageStartTime += time; + this.stageProgress = 0; + } + + private aiPrologue(time: number, frame: number) { + // stageProgress: + // 0: 开始; 1: 开始血条动画 + + this.healthBar.showStart(); + this.stageProgress = 1; + + if (time > 1500) { + this.changeStage(TowerBossStage.Stage1, time); + this.attackTime = 2; + this.skill1Time = 1; + this.skill2Time = 1; + this.skill3Time = 1; + } + } + + async releaseSkill1() { + const locs = new Set(); + const count = Math.ceil(Math.random() * 8) + 4; + let i = 0; + while (i < count) { + const dir = Math.floor(Math.random() * 2); + const pos = Math.floor(Math.random() * 13 + 1); + const loc = pos + dir * 13; + if (!locs.has(loc)) continue; + i++; + locs.add(loc); + const proj = this.createProjectile(ArrowProjectile, 0, 0); + proj.setData(dir); + if (dir === ProjectileDirection.Horizontal) { + proj.setPosition(480 - 32, pos * 32 + 32); + } else { + proj.setPosition(pos * 32 + 32, 480 - 32); + } + await sleep(200); + } + } + + releaseSkill2() { + const x = Math.floor(Math.random() * 13 + 1); + const y = Math.floor(Math.random() * 13 + 1); + const proj = this.createProjectile(PortalProjectile, 0, 0); + proj.setTarget(x, y); + proj.createEffect(TowerBoss.effect); + } + + async releaseSkill3() {} + + private aiStage1(time: number, frame: number) { + // stageProgress: + // 0: 开始; 1,2,3,4: 对应对话 + + const skill1Release = this.skill1Time * this.skill1Interval; + const skill2Release = this.skill2Time * this.skill2Interval; + const skill3Release = this.skill3Time * this.skill3Interval; + const attack = this.attackTime * this.attackInterval; + + if (time > skill1Release) { + this.releaseSkill1(); + this.skill1Time++; + } + if (time > skill2Release) { + this.releaseSkill2(); + this.skill2Time++; + } + if (time > skill3Release) { + this.releaseSkill3(); + this.skill3Time++; + } + if (time > attack) { + this.addAttackCircle(3000, 500); + this.attackTime++; + } + + if (this.hp <= 7000) { + this.changeStage(TowerBossStage.Dialogue1, time); + this.attackTime = 1; + } + } + + private aiDialogue1(time: number, frame: number) {} + + private aiStage2(time: number, frame: number) {} + + private aiDialogue2(time: number, frame: number) {} + + private aiStage3(time: number, frame: number) {} + + private aiDialogue3(time: number, frame: number) {} + + private aiStage4(time: number, frame: number) {} + + private aiStage5(time: number, frame: number) {} + + private aiStage6(time: number, frame: number) {} + + private aiEnd(time: number, frame: number) {} +} + +class BossEffect extends BossSprite { + private attackCircle: AttackCircleRenderable[] = []; + + /** + * 初始化 + */ + init() { + this.size(480, 480); + this.setHD(true); + this.setZIndex(80); + } + + /** + * 设置攻击boss圆圈的渲染信息 + */ + setAttackCircle(renderable: AttackCircleRenderable[]) { + this.attackCircle = renderable; + } + + protected preDraw( + canvas: MotaOffscreenCanvas2D, + transform: Transform + ): boolean { + this.renderAttackCircle(canvas); + return true; + } + + protected postDraw( + canvas: MotaOffscreenCanvas2D, + transform: Transform + ): void {} + + private renderAttackCircle(canvas: MotaOffscreenCanvas2D) { + const ctx = canvas.ctx; + ctx.strokeStyle = '#ffe229'; + ctx.fillStyle = '#ffe229'; + ctx.lineWidth = 2; + this.attackCircle.forEach(({ cx, cy, lineOffset, alpha }) => { + ctx.globalAlpha = alpha; + ctx.beginPath(); + const offset = lineOffset + 8; + ctx.arc(cx, cy, 2, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.arc(cx, cy, offset, 0, Math.PI * 2); + ctx.moveTo(cx + offset, cy); + ctx.lineTo(cx + offset + 16, cy); + ctx.moveTo(cx, cy + offset); + ctx.lineTo(cx, cy + offset + 16); + ctx.moveTo(cx - offset, cy); + ctx.lineTo(cx - offset - 16, cy); + ctx.moveTo(cx, cy - offset); + ctx.lineTo(cx, cy - offset - 16); + ctx.stroke(); + }); + ctx.globalAlpha = 1; + } } interface TextRenderable { @@ -80,8 +500,6 @@ class Word extends RenderItem { /** 当前正在显示的文字 */ private showing: string = ''; - /** 是否已经显示完毕 */ - private showEnd: boolean = true; /** 文字显示时间间隔 */ private showInterval: number = 100; /** 文字显示的虚化时长 */ @@ -128,7 +546,6 @@ class Word extends RenderItem { * @param text 要显示的文字 */ showText(text: string) { - this.showEnd = false; this.showStartTime = Date.now(); this.showing = text; } diff --git a/src/plugin/boss/towerBossProjectile.ts b/src/plugin/boss/towerBossProjectile.ts new file mode 100644 index 0000000..9b33dd8 --- /dev/null +++ b/src/plugin/boss/towerBossProjectile.ts @@ -0,0 +1,295 @@ +import { hyper, power, TimingFn } from 'mutate-animate'; +import { Hitbox, Projectile } from './barrage'; +import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; +import { Transform } from '@/core/render/transform'; +import type { TowerBoss } from './towerBoss'; +import { IStateDamageable } from '@/game/state/interface'; +import { PointEffect, PointEffectType } from '../fx/pointShader'; +import { isNil } from 'lodash-es'; + +export const enum ProjectileDirection { + Vertical, + Horizontal +} + +export class ArrowProjectile extends Projectile { + static easing?: TimingFn; + static dangerEasing?: TimingFn; + + static horizontal: MotaOffscreenCanvas2D | null = null; + static vertical: MotaOffscreenCanvas2D | null = null; + + hitbox: Hitbox.Rect = new Hitbox.Rect(0, 0, 102, 32); + damage: number = 1000; + + /** 弹幕的方向 */ + direction: ProjectileDirection = ProjectileDirection.Horizontal; + + private damaged: boolean = false; + + /** + * boss战开始时初始化 + */ + static init() { + this.easing = power(2, 'in'); + this.dangerEasing = power(3, 'out'); + this.horizontal = new MotaOffscreenCanvas2D(); + this.vertical = new MotaOffscreenCanvas2D(); + const hor = this.horizontal; + hor.size(480 - 64, 32); + hor.setHD(true); + const ctxHor = hor.ctx; + ctxHor.fillStyle = '#f00'; + ctxHor.globalAlpha = 0.6; + for (let i = 0; i < 13; i++) { + ctxHor.fillRect(i * 32 + 2, 2, 28, 28); + } + const ver = this.vertical; + ver.size(480 - 64, 32); + ver.setHD(true); + const ctxVer = ver.ctx; + ctxVer.fillStyle = '#f00'; + ctxVer.globalAlpha = 0.6; + for (let i = 0; i < 13; i++) { + ctxVer.fillRect(2, i * 32 + 2, 28, 28); + } + } + + /** + * boss战结束后清理 + */ + static end() { + this.easing = void 0; + this.dangerEasing = void 0; + this.horizontal?.clear(); + this.horizontal = null; + this.vertical?.clear(); + this.vertical = null; + } + + /** + * 设置弹幕的数据 + * @param direction 弹幕的方向 + */ + setData(direction: ProjectileDirection) { + this.direction = direction; + } + + isIntersect(hitbox: Hitbox.HitboxType): boolean { + if (hitbox instanceof Hitbox.Rect) { + return Hitbox.checkRectRect(hitbox, this.hitbox); + } else { + return false; + } + } + + updateHitbox(x: number, y: number): void { + this.hitbox.setPosition(x, y); + } + + doDamage(target: IStateDamageable): boolean { + if (this.damaged) return false; + target.hp -= this.damage; + this.damaged = true; + return true; + } + + ai(boss: TowerBoss, time: number, frame: number): void { + if (time > 3000) { + const progress = (time - 3000) / 2000; + const res = ArrowProjectile.easing!(progress); + const dx = res * 640; + const x = 480 - 32 - dx; + if (this.direction === ProjectileDirection.Horizontal) { + this.setPosition(this.x, x); + } else { + this.setPosition(x, this.y); + } + } else if (time > 5000) { + this.destroy(); + } + } + + render(canvas: MotaOffscreenCanvas2D, transform: Transform): void { + const ctx = canvas.ctx; + + if (this.time < 3000) { + let begin = 1; + if (this.time < 2000) { + begin = ArrowProjectile.dangerEasing!(this.time / 2000); + } + ctx.beginPath(); + const len = begin * 13 * 32; + const x1 = 480 - 32 - len; + + if (this.direction === ProjectileDirection.Horizontal) { + const canvas = ArrowProjectile.horizontal!.canvas; + ctx.drawImage(canvas, x1, this.y, len, 32); + } else { + const canvas = ArrowProjectile.vertical!.canvas; + ctx.drawImage(canvas, this.y, x1, 32, len); + } + } else { + const len = Math.max(this.y - 32, 0); + if (this.direction === ProjectileDirection.Horizontal) { + const canvas = ArrowProjectile.horizontal!.canvas; + ctx.drawImage(canvas, 32, this.y, len, 32); + } else { + const canvas = ArrowProjectile.vertical!.canvas; + ctx.drawImage(canvas, this.y, 32, 32, len); + } + } + const img = core.material.images.images['arrow.png']; + ctx.drawImage(img, this.x, this.y, 102, 32); + } +} + +export class PortalProjectile extends Projectile { + static easing?: TimingFn; + + damage: number = 0; + hitbox: Hitbox.Circle = new Hitbox.Circle(0, 0, 0); + + /** 传送目标位置 */ + private tx: number = 0; + /** 传送目标位置 */ + private ty: number = 0; + /** 是否已经传送过 */ + private transfered: boolean = false; + + private effect?: PointEffect; + private effectId?: number; + + static init() { + this.easing = hyper('sin', 'out'); + } + + static end() { + this.easing = void 0; + } + + createEffect(effect: PointEffect) { + this.effect = effect; + const id = effect.addEffect( + PointEffectType.CircleWarpTangetial, + Date.now(), + 4000, + [this.tx * 32, this.ty * 32, 12, 20] + ); + this.effectId = id; + } + + /** + * 设置传送目标位置 + */ + setTarget(x: number, y: number) { + this.tx = x; + this.ty = y; + } + + isIntersect(hitbox: Hitbox.HitboxType): boolean { + return false; + } + + updateHitbox(x: number, y: number): void { + this.hitbox.setCenter(x, y); + } + + doDamage(target: IStateDamageable): boolean { + return false; + } + + ai(boss: TowerBoss, time: number, frame: number): void { + if (!this.transfered && time > 2000) { + this.transfered = true; + core.setHeroLoc('x', this.tx); + core.setHeroLoc('y', this.ty); + } + + if (time > 4000) { + this.destroy(); + } + } + + render(canvas: MotaOffscreenCanvas2D, transform: Transform): void { + const effect = this.effect; + const id = this.effectId; + if (!effect || isNil(id)) return; + const time = this.time; + const max = Math.PI * 8; + if (time < 2000) { + const progress = PortalProjectile.easing!(time / 2000); + effect.setEffect(id, void 0, [0, max * progress, 0, 0]); + } else { + const progress = PortalProjectile.easing!((time - 2000) / 2000); + effect.setEffect(id, void 0, [max * progress, max, 0, 0]); + } + } +} + +export class IceProjectile extends Projectile { + damage: number = 5000; + hitbox: Hitbox.Rect = new Hitbox.Rect(0, 0, 32, 32); + + private damaged: boolean = false; + private animated: boolean = false; + private converted: boolean = false; + + private bx: number = 0; + private by: number = 0; + + setPos(x: number, y: number) { + this.bx = x; + this.by = y; + } + + isIntersect(hitbox: Hitbox.HitboxType): boolean { + if (this.damaged) return false; + if (this.time < 2000) return false; + if (hitbox instanceof Hitbox.Rect) { + return Hitbox.checkRectRect(hitbox, this.hitbox); + } else { + return false; + } + } + + updateHitbox(x: number, y: number): void { + this.hitbox.setPosition(x, y); + } + + doDamage(target: IStateDamageable): boolean { + if (!this.damaged) return false; + target.hp -= this.damage; + this.damaged = true; + return true; + } + + ai(boss: TowerBoss, time: number, frame: number): void { + if (!this.converted && time > 2000) { + this.converted = true; + core.setBgFgBlock('bg', 167, this.bx, this.by); + } + if (time > 4000) { + core.setBgFgBlock('bg', 526, this.bx, this.by); + this.destroy(); + } + } + + render(canvas: MotaOffscreenCanvas2D, transform: Transform): void { + const ctx = canvas.ctx; + if (this.time < 2000) { + const fill = ctx.fillStyle; + const alpha = ctx.globalAlpha; + ctx.fillStyle = 'rgb(150,150,255)'; + ctx.globalAlpha = 0.6; + ctx.fillRect(this.x + 2, this.y + 2, 28, 28); + ctx.fillStyle = fill; + ctx.globalAlpha = alpha; + } else { + if (!this.animated) { + this.animated = true; + core.drawAnimate('ice', this.bx, this.by); + } + } + } +}