diff --git a/src/plugin/boss/towerBoss.ts b/src/plugin/boss/towerBoss.ts index 6779f57..ad3b9bb 100644 --- a/src/plugin/boss/towerBoss.ts +++ b/src/plugin/boss/towerBoss.ts @@ -17,8 +17,11 @@ import { import { Container } from '@/core/render/container'; import { ArrowProjectile, + IceProjectile, PortalProjectile, - ProjectileDirection + ProjectileDirection, + ThunderBallProjectile, + ThunderProjectile } from './towerBossProjectile'; import { IStateDamageable } from '@/game/state/interface'; @@ -39,10 +42,8 @@ const enum TowerBossStage { Stage2, Dialogue2, Stage3, - Dialogue3, Stage4, Stage5, - Stage6, End } @@ -171,6 +172,7 @@ export class TowerBoss extends BarrageBoss { ArrowProjectile.init(); PortalProjectile.init(); + ThunderProjectile.init(); } override end() { @@ -183,6 +185,7 @@ export class TowerBoss extends BarrageBoss { ArrowProjectile.end(); PortalProjectile.end(); + ThunderProjectile.end(); } /** @@ -208,11 +211,11 @@ export class TowerBoss extends BarrageBoss { this.hp -= damage; this.healthBar.set(this.hp); // 先用drawAnimate凑活一下,等下个版本提供更好的 api - if (this.stage === TowerBossStage.Stage4) { + if (this.stage === TowerBossStage.Stage3) { core.drawAnimate('hand', 7, 2); - } else if (this.stage === TowerBossStage.Stage5) { + } else if (this.stage === TowerBossStage.Stage4) { core.drawAnimate('hand', 7, 3); - } else if (this.stage === TowerBossStage.Stage6) { + } else if (this.stage === TowerBossStage.Stage5) { core.drawAnimate('hand', 7, 4); } else { core.drawAnimate('hand', 7, 1); @@ -227,13 +230,13 @@ export class TowerBoss extends BarrageBoss { addAttackCircle(last: number, damage: number) { let nx = 0; let ny = 0; - if (this.stage === TowerBossStage.Stage4) { + if (this.stage === TowerBossStage.Stage3) { nx = Math.floor(Math.random() * 11 + 2); ny = Math.floor(Math.random() * 11 + 2); - } else if (this.stage === TowerBossStage.Stage5) { + } else if (this.stage === TowerBossStage.Stage4) { nx = Math.floor(Math.random() * 9 + 3); ny = Math.floor(Math.random() * 9 + 3); - } else if (this.stage === TowerBossStage.Stage6) { + } else if (this.stage === TowerBossStage.Stage5) { nx = Math.floor(Math.random() * 7 + 4); ny = Math.floor(Math.random() * 7 + 4); } else { @@ -301,18 +304,12 @@ export class TowerBoss extends BarrageBoss { 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; @@ -376,7 +373,20 @@ export class TowerBoss extends BarrageBoss { proj.createEffect(TowerBoss.effect); } - async releaseSkill3() {} + async releaseSkill3() { + const count = Math.floor(Math.random() * 100); + const used = new Set(); + for (let i = 0; i < count; i++) { + const x = Math.floor(Math.random() * 13 + 1); + const y = Math.floor(Math.random() * 13 + 1); + const index = x + y * 13; + if (used.has(index)) continue; + used.add(index); + const proj = this.createProjectile(IceProjectile, x * 32, y * 32); + proj.setPos(x, y); + await sleep(20); + } + } private aiStage1(time: number, frame: number) { // stageProgress: @@ -410,22 +420,119 @@ export class TowerBoss extends BarrageBoss { } } - private aiDialogue1(time: number, frame: number) {} + private aiDialogue1(time: number, frame: number) { + this.changeStage(TowerBossStage.Stage2, time); + this.attackTime = 3; + this.skill4Time = 5; + this.skill5Time = 3; + } - private aiStage2(time: number, frame: number) {} + releaseSkill4() { + const x = Math.floor(Math.random() * 11 + 2); + const y = Math.floor(Math.random() * 11 + 2); + const power = Math.floor(Math.random() * 6 + 1); + const proj = this.createProjectile(ThunderProjectile, 0, 0); + proj.setData(x, y, power); + proj.createEffect(TowerBoss.effect); + } - private aiDialogue2(time: number, frame: number) {} + async releaseSkill5() { + const count = Math.floor(Math.random() * 12 + 6); + const used = new Set(); + let i = 0; + while (i < count) { + const x = Math.floor(Math.random() * 13 + 1); + const y = Math.floor(Math.random() * 13 + 1); + const index = x + y * 13; + if (used.has(index)) continue; + i++; + used.add(index); + const px = x * 32 + 16; + const py = y * 32 + 16; + const proj1 = this.createProjectile(ThunderBallProjectile, 0, 0); + const proj2 = this.createProjectile(ThunderBallProjectile, 0, 0); + const proj3 = this.createProjectile(ThunderBallProjectile, 0, 0); + const proj4 = this.createProjectile(ThunderBallProjectile, 0, 0); + proj1.setData(ProjectileDirection.BottomToTop, x, y); + proj2.setData(ProjectileDirection.LeftToRight, x, y); + proj3.setData(ProjectileDirection.RightToLeft, x, y); + proj4.setData(ProjectileDirection.TopToBottom, x, y); + proj1.setPosition(px, py); + proj2.setPosition(px, py); + proj3.setPosition(px, py); + proj4.setPosition(px, py); + await sleep(200); + } + } + + private aiStage2(time: number, frame: number) { + const skill4Release = this.skill4Time * this.skill4Interval; + const skill5Release = this.skill5Time * this.skill5Interval; + const attack = this.attackTime * this.attackInterval; + + if (time > skill4Release) { + this.releaseSkill4(); + this.skill4Time++; + } + if (time > skill5Release) { + this.releaseSkill5(); + this.skill5Time++; + } + if (time > attack) { + this.addAttackCircle(3000, 500); + this.attackTime++; + } + + if (this.hp <= 3500) { + this.changeStage(TowerBossStage.Dialogue2, time); + this.attackTime = 1; + } + } + + /** + * 压缩地形,将地形向内压缩一格 + */ + terrainClose(n: number) { + for (let nx = n - 1; nx < 15 - n + 1; nx++) { + core.removeBlock(nx, n - 1); + core.removeBlock(nx, 15 - n + 1); + core.setBgFgBlock('bg', 0, nx, n - 1); + core.setBgFgBlock('bg', 0, nx, 15 - n + 1); + } + for (let ny = n; ny < 15 - n; ny++) { + core.removeBlock(n - 1, ny); + core.removeBlock(15 - n + 1, ny); + core.setBgFgBlock('bg', 0, n - 1, ny); + core.setBgFgBlock('bg', 0, 15 - n + 1, ny); + } + for (let nx = n; nx < 15 - n; nx++) { + core.setBlock(527, nx, n); + core.setBlock(527, nx, 15 - n); + } + for (let ny = n + 1; ny < 15 - n - 1; ny++) { + core.setBlock(527, n, ny); + core.setBlock(527, 15 - n, ny); + } + } + + private aiDialogue2(time: number, frame: number) { + this.changeStage(TowerBossStage.Stage3, time); + this.attackTime = 3; + this.terrainClose(1); + this.skill6Time = 30; + this.skill7Time = 2; + } + + releaseSkill6() {} + + releaseSkill7() {} 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) {} } diff --git a/src/plugin/boss/towerBossProjectile.ts b/src/plugin/boss/towerBossProjectile.ts index 9b33dd8..2a2f2a6 100644 --- a/src/plugin/boss/towerBossProjectile.ts +++ b/src/plugin/boss/towerBossProjectile.ts @@ -9,7 +9,12 @@ import { isNil } from 'lodash-es'; export const enum ProjectileDirection { Vertical, - Horizontal + Horizontal, + + LeftToRight, + RightToLeft, + TopToBottom, + BottomToTop } export class ArrowProjectile extends Projectile { @@ -38,6 +43,7 @@ export class ArrowProjectile extends Projectile { const hor = this.horizontal; hor.size(480 - 64, 32); hor.setHD(true); + hor.withGameScale(true); const ctxHor = hor.ctx; ctxHor.fillStyle = '#f00'; ctxHor.globalAlpha = 0.6; @@ -47,6 +53,7 @@ export class ArrowProjectile extends Projectile { const ver = this.vertical; ver.size(480 - 64, 32); ver.setHD(true); + ver.withGameScale(true); const ctxVer = ver.ctx; ctxVer.fillStyle = '#f00'; ctxVer.globalAlpha = 0.6; @@ -118,25 +125,24 @@ export class ArrowProjectile extends Projectile { 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); + ctx.drawImage(canvas, x1, 0, len, 32, x1, this.y, len, 32); } else { const canvas = ArrowProjectile.vertical!.canvas; - ctx.drawImage(canvas, this.y, x1, 32, len); + ctx.drawImage(canvas, 0, x1, 32, len, 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); + ctx.drawImage(canvas, 32, 0, len, 32, 32, this.y, len, 32); } else { const canvas = ArrowProjectile.vertical!.canvas; - ctx.drawImage(canvas, this.y, 32, 32, len); + ctx.drawImage(canvas, 0, 32, 32, len, this.y, 32, 32, len); } } const img = core.material.images.images['arrow.png']; @@ -232,15 +238,21 @@ export class IceProjectile extends Projectile { 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; + this.updateHitbox(x * 32, y * 32); } isIntersect(hitbox: Hitbox.HitboxType): boolean { @@ -293,3 +305,405 @@ export class IceProjectile extends Projectile { } } } + +export class ThunderProjectile extends Projectile { + /** 闪电缓存画布 */ + static cache: MotaOffscreenCanvas2D | null = null; + + damage: number = 0; + hitbox: Hitbox.Rect = new Hitbox.Rect(0, 0, 96, 96); + + private bx: number = 0; + private by: number = 0; + /** 闪电的强度 */ + private power: number = 0; + private damaged: boolean = false; + private cached: boolean = false; + + private effect?: PointEffect; + private effectId?: number; + + static init() { + this.cache = new MotaOffscreenCanvas2D(); + this.cache.setHD(true); + this.cache.withGameScale(true); + } + + static end() { + this.cache?.clear(); + this.cache = null; + } + + /** + * 创建着色器特效 + */ + createEffect(effect: PointEffect) { + this.effect = effect; + this.effectId = effect.addEffect( + PointEffectType.CircleBrightness, + Date.now() + 1000, + 400, + [this.bx * 32 + 32, this.by * 32 + 32, 128, 32] + ); + } + + /** + * 设置闪电的信息 + */ + setData(x: number, y: number, power: number) { + this.bx = x; + this.by = y; + this.power = power; + this.damage = power * 3000; + this.updateHitbox(x * 32 - 32, y * 32 - 32); + } + + isIntersect(hitbox: Hitbox.HitboxType): boolean { + if (this.damaged) return false; + if (this.time < 1000) 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; + this.damaged = true; + target.hp -= this.damage; + return true; + } + + ai(boss: TowerBoss, time: number, frame: number): void { + if (time > 2000) { + this.destroy(); + } + } + + render(canvas: MotaOffscreenCanvas2D, transform: Transform): void { + const ctx = canvas.ctx; + if (this.time < 1000) { + const before = ctx.fillStyle; + ctx.fillStyle = '#fff'; + for (let dx = -1; dx < 2; dx++) { + for (let dy = -1; dy < 2; dy++) { + const x = (this.bx + dx) * 32 + 2; + const y = (this.by + dy) * 32 + 2; + ctx.fillRect(x, y, 28, 28); + } + } + ctx.fillStyle = before; + } else { + if (!this.cached) this.cacheThunder(); + if (!ThunderProjectile.cache) return; + const x = this.bx * 32; + const before = ctx.globalAlpha; + const progress = (this.time - 1000) / 1000; + if (progress < 0.4) { + const effect = this.effect; + const id = this.effectId; + if (!effect || isNil(id)) return; + const effectRatio = ArrowProjectile.dangerEasing!( + progress * 2.5 + ); + effect.setEffect(id, void 0, [effectRatio, 0, 0, 0]); + } + ctx.globalAlpha = 1 - progress; + ctx.drawImage(ThunderProjectile.cache.canvas, x - 60, 0); + ctx.globalAlpha = before; + } + } + + private cacheThunder() { + const cache = ThunderProjectile.cache; + if (!cache) return; + const bottom = this.by * 32 + 32; + cache.size(120, bottom); + const ctx = cache.ctx; + ctx.beginPath(); + for (let i = 0; i < this.power; i++) { + let x = this.bx * 32; + let y = this.by * 32; + ctx.moveTo(x, y); + while (y > 0) { + x += Math.floor(Math.random() * 30 - 15); + y -= Math.floor(Math.random() * 80); + ctx.lineTo(x, y); + } + } + ctx.shadowBlur = 3; + ctx.shadowColor = '#62c8f4'; + ctx.lineWidth = 2; + ctx.globalAlpha = 0.6; + ctx.stroke(); + } +} + +export class ThunderBallProjectile extends Projectile { + static dangerEasing?: TimingFn; + + static horizontal: MotaOffscreenCanvas2D | null = null; + static vertical: MotaOffscreenCanvas2D | null = null; + + damage: number = 3000; + hitbox: Hitbox.Rect = new Hitbox.Rect(0, 0, 16, 16); + + private direction: ProjectileDirection = ProjectileDirection.BottomToTop; + private cx: number = 0; + private cy: number = 0; + private damaged: boolean = false; + + /** + * boss战开始时初始化 + */ + static init() { + 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); + hor.withGameScale(true); + const ctxHor = hor.ctx; + ctxHor.fillStyle = '#fff'; + 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); + ver.withGameScale(true); + const ctxVer = ver.ctx; + ctxVer.fillStyle = '#fff'; + ctxVer.globalAlpha = 0.6; + for (let i = 0; i < 13; i++) { + ctxVer.fillRect(2, i * 32 + 2, 28, 28); + } + } + + /** + * boss战结束后清理 + */ + static end() { + this.dangerEasing = void 0; + this.horizontal?.clear(); + this.horizontal = null; + this.vertical?.clear(); + this.vertical = null; + } + + setData(direction: ProjectileDirection, cx: number, cy: number) { + this.cx = cx; + this.cy = cy; + this.direction = direction; + this.setPosition(cx * 32 + 16, cy * 32 + 16); + } + + isIntersect(hitbox: Hitbox.HitboxType): boolean { + if (this.damaged) return false; + if (this.time < 3000) return false; + if (hitbox instanceof Hitbox.Rect) { + return Hitbox.checkRectRect(this.hitbox, hitbox); + } else { + return false; + } + } + + updateHitbox(x: number, y: number): void { + this.hitbox.setPosition(x, y); + } + + doDamage(target: IStateDamageable): boolean { + if (this.damaged) return false; + this.damaged = true; + target.hp -= this.damage; + return true; + } + + ai(boss: TowerBoss, time: number, frame: number): void { + if (time > 3000) { + const dt = time - 3000; + const dis = dt * 0.2; + const cx = this.cx * 32 + 16; + const cy = this.cy * 32 + 16; + + switch (this.direction) { + case ProjectileDirection.BottomToTop: + this.setPosition(cx, cy - dis); + break; + case ProjectileDirection.LeftToRight: + this.setPosition(cx + dis, cy); + break; + case ProjectileDirection.RightToLeft: + this.setPosition(cx - dis, cy); + break; + case ProjectileDirection.TopToBottom: + this.setPosition(cx, cy + dis); + break; + } + + if (this.x < -16 || this.x > 496 || this.y < -16 || this.y > 496) { + this.destroy(); + } + } + } + + render(canvas: MotaOffscreenCanvas2D, transform: Transform): void { + const ctx = canvas.ctx; + const cx = this.cx * 32 + 16; + const cy = this.cy * 32 + 16; + let left = 0; + let right = 0; + let top = 0; + let bottom = 0; + if (this.time < 3000) { + let begin = 1; + if (this.time < 2000) { + begin = ArrowProjectile.dangerEasing!(this.time / 2000); + } + + switch (this.direction) { + case ProjectileDirection.BottomToTop: { + const height = (cy - 48) * begin; + left = cx - 16; + right = cx + 16; + bottom = cy + 16; + top = cy - height - 16; + break; + } + case ProjectileDirection.LeftToRight: { + const width = (432 - cx) * begin; + left = cx - 16; + right = cx + 16 + width; + bottom = cy + 16; + top = cy - 16; + break; + } + case ProjectileDirection.RightToLeft: { + const width = (cx - 48) * begin; + left = cx - width - 16; + right = cx + 16; + bottom = cy + 16; + top = cy - 16; + break; + } + case ProjectileDirection.TopToBottom: { + const height = (432 - cy) * begin; + left = cx - 16; + right = cx + 16; + bottom = cy + 16; + top = cy + 16 + height; + break; + } + } + } else { + switch (this.direction) { + case ProjectileDirection.BottomToTop: { + left = cx - 16; + right = cx + 16; + bottom = this.y; + top = 32; + break; + } + case ProjectileDirection.LeftToRight: { + left = this.x; + right = 448; + bottom = cy + 16; + top = cy - 16; + break; + } + case ProjectileDirection.RightToLeft: { + left = 32; + right = this.x; + bottom = cy + 16; + top = cy - 16; + break; + } + case ProjectileDirection.TopToBottom: { + left = cx - 16; + right = cx + 16; + bottom = 448; + top = this.y; + break; + } + } + } + const w = right - left; + const h = bottom - top; + const hor = ThunderBallProjectile.horizontal!.canvas; + const ver = ThunderBallProjectile.vertical!.canvas; + switch (this.direction) { + case ProjectileDirection.BottomToTop: + case ProjectileDirection.TopToBottom: { + ctx.drawImage(hor, 0, top, 32, h, left, top, w, h); + break; + } + case ProjectileDirection.LeftToRight: + case ProjectileDirection.RightToLeft: { + ctx.drawImage(ver, left, 0, w, 32, left, top, w, h); + } + } + ctx.fillStyle = '#fff'; + ctx.globalAlpha = 0.9; + ctx.beginPath(); + const radius = 9 + Math.floor(Math.random() * 8 - 4); + ctx.arc(this.x, this.y, radius, 0, Math.PI * 2); + ctx.fill(); + } +} + +export class BoomProjectile extends Projectile { + damage: number = 3000; + hitbox: Hitbox.Rect = new Hitbox.Rect(0, 0, 32, 32); + + isIntersect(hitbox: Hitbox.HitboxType): boolean { + throw new Error('Method not implemented.'); + } + + updateHitbox(x: number, y: number): void { + throw new Error('Method not implemented.'); + } + + doDamage(target: IStateDamageable): boolean { + throw new Error('Method not implemented.'); + } + + ai(boss: TowerBoss, time: number, frame: number): void { + throw new Error('Method not implemented.'); + } + + render(canvas: MotaOffscreenCanvas2D, transform: Transform): void { + throw new Error('Method not implemented.'); + } +} + +export class ChainProjectile extends Projectile { + damage: number = 4000; + hitbox: Hitbox.Line = new Hitbox.Line(0, 0, 0, 0); + + isIntersect(hitbox: Hitbox.HitboxType): boolean { + throw new Error('Method not implemented.'); + } + + updateHitbox(x: number, y: number): void { + throw new Error('Method not implemented.'); + } + + doDamage(target: IStateDamageable): boolean { + throw new Error('Method not implemented.'); + } + + ai(boss: TowerBoss, time: number, frame: number): void { + throw new Error('Method not implemented.'); + } + + render(canvas: MotaOffscreenCanvas2D, transform: Transform): void { + throw new Error('Method not implemented.'); + } +} diff --git a/src/plugin/fx/pointShader.ts b/src/plugin/fx/pointShader.ts index 5281553..c1e0231 100644 --- a/src/plugin/fx/pointShader.ts +++ b/src/plugin/fx/pointShader.ts @@ -43,7 +43,7 @@ export const enum PointEffectType { */ CircleHue, /** - * 圆形扭曲特效,注意此特效会导致在此之前的所有非扭曲类特效失效,在添加时,系统会自动排序以保证特效正常显示\ + * 圆形扭曲特效\ * 参数分别为:\ * `data1: x, y, maxRaius, waveRadius` | 中心横坐标,中心纵坐标,波纹最大传播距离,波纹环的半径\ * `data2: amplitude, attenuation, linear, tangential` \ @@ -63,7 +63,14 @@ export const enum PointEffectType { * (1表示扭曲了相位角的程度,例如Math.PI的相位,幅度为1,表示旋转整整一圈)\ * `data2: startPhase, endPhase, _, _` 起始位置相位(靠近波纹中心的位置),终止位置相位(远离波纹中心的位置),空,空 */ - CircleWarpTangetial + CircleWarpTangetial, + /** + * 圆形亮度特效,可与任何特效叠加\ + * 参数分别为:\ + * `data1: x, y, radius, decay` | 中心横坐标,中心纵坐标,半径,衰减开始半径\ + * `data2: ratio, _, _, _` | 亮度(0表示不变,1表示2倍亮度),空,空,空 + */ + CircleBrightness } type EffectData = [x0: number, x1: number, x2: number, x3: number]; @@ -568,6 +575,13 @@ void main() { color = vec4(color.rgb - grayed * ratio, 1.0); } } + // 亮度,data1: x y radius decay;data2: ratio _ _ _ + else if (effectType == ${PointEffectType.CircleBrightness}) { + float ratio = data2.x * calCircleDecay(data1) + 1.0; + if (ratio > 0.0) { + color.rgb *= ratio; + } + } // 对比度,data1: x y radius decay;data2: ratio _ _ _ else if (effectType == ${PointEffectType.CircleContrast}) { float ratio = data2.x * calCircleDecay(data1) + 1.0;