feat: 智慧塔弹幕战第二阶段

This commit is contained in:
unanmed 2024-11-16 23:14:04 +08:00
parent d9a398a8e9
commit b5b435d133
3 changed files with 566 additions and 31 deletions

View File

@ -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<number>();
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<number>();
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) {}
}

View File

@ -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<TowerBoss> {
@ -38,6 +43,7 @@ export class ArrowProjectile extends Projectile<TowerBoss> {
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<TowerBoss> {
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<TowerBoss> {
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<TowerBoss> {
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<TowerBoss> {
}
}
}
export class ThunderProjectile extends Projectile<TowerBoss> {
/** 闪电缓存画布 */
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<TowerBoss> {
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<TowerBoss> {
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<TowerBoss> {
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.');
}
}

View File

@ -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 {
* 1Math.PI的相位1\
* `data2: startPhase, endPhase, _, _`
*/
CircleWarpTangetial
CircleWarpTangetial,
/**
* \
* \
* `data1: x, y, radius, decay` | \
* `data2: ratio, _, _, _` | 012
*/
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 decaydata2: 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 decaydata2: ratio _ _ _
else if (effectType == ${PointEffectType.CircleContrast}) {
float ratio = data2.x * calCircleDecay(data1) + 1.0;