HumanBreak/src/plugin/boss/towerBoss.ts

845 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Shader } from '@/core/render/shader';
import { PointEffect } from '../fx/pointShader';
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 { Animation, hyper, power, sleep, Transition } from 'mutate-animate';
import { Container } from '@/core/render/container';
import {
ArrowProjectile,
AttackProjectile,
BoomProjectile,
ChainProjectile,
IceProjectile,
PortalProjectile,
ProjectileDirection,
ThunderBallProjectile,
ThunderProjectile
} from './towerBossProjectile';
import { IStateDamageable } from '@/game/state/interface';
import { HeroRenderer } from '@/core/render/preset/hero';
import { Pop } from '../fx/pop';
import { WeatherController } from '@/module';
Mota.require('var', 'loading').once('coreInit', () => {
const shader = new Shader();
shader.size(480, 480);
shader.setHD(true);
shader.setZIndex(120);
TowerBoss.shader = shader;
TowerBoss.effect.create(shader, 40);
});
const enum TowerBossStage {
/** 开场白阶段 */
Prologue,
Stage1,
Dialogue1,
Stage2,
Dialogue2,
Stage3,
Stage4,
Stage5,
End
}
const enum HealthBarStatus {
Start,
Running,
End
}
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;
readonly hitbox: Hitbox.Rect;
readonly state: IStateDamageable;
readonly main: BossSprite;
/** 血条显示元素 */
private healthBar: HealthBar;
/** 对话文字显示元素 */
private word: Word;
/** 楼层渲染元素 */
private group: LayerGroup;
/** 楼层渲染容器 */
private mapDraw: Container;
/** 伤害弹出 */
pop: Pop;
/** 每个阶段的进度,具体定义参考 ai 函数开头 */
private stageProgress: number = 0;
/** 当前阶段的开始时刻 */
private stageStartTime: number = 0;
/** 每一阶段的攻击boss次数 */
private attackTime: number = 0;
/** 攻击boss的红圈间隔时长 */
private attackInterval: number = 7000;
/** 使用技能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;
private heroHp: number = 0;
constructor() {
super();
this.healthBar = new HealthBar('absolute');
this.word = new Word('absolute');
this.main = new BossSprite('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.pop = render.getElementById('pop-main') as Pop;
this.healthBar.init();
this.word.init();
this.main.size(480, 480);
this.main.setHD(true);
this.main.setZIndex(80);
TowerBoss.effect.setTransform(this.group.camera);
const { x, y } = core.status.hero.loc;
const cell = 32;
this.hitbox = new Hitbox.Rect(x * cell + 2, y * cell + 2, 28, 28);
this.state = core.status.hero;
}
private moveTick = (x: number, y: number) => {
this.hitbox.setPosition(x * 32 + 2, y * 32 + 2);
};
override start() {
super.start();
TowerBoss.shader.append(this.mapDraw);
this.healthBar.append(this.group);
this.word.append(this.group);
this.main.append(this.group);
const event = this.group.getLayer('event');
const hero = event?.getExtends('floor-hero') as HeroRenderer;
hero?.on('moveTick', this.moveTick);
ArrowProjectile.init();
PortalProjectile.init();
ThunderProjectile.init();
ThunderBallProjectile.init();
AttackProjectile.init();
TowerBoss.effect.start();
TowerBoss.effect.use();
this.heroHp = core.status.hero.hp;
}
override end() {
super.end();
TowerBoss.shader.remove();
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;
hero?.off('moveTick', this.moveTick);
ArrowProjectile.end();
PortalProjectile.end();
ThunderProjectile.end();
ThunderBallProjectile.end();
AttackProjectile.end();
TowerBoss.effect.end();
core.status.hero.hp = this.heroHp;
Mota.Plugin.require('replay_g').clip('choices:0');
}
/**
* 用于全局检测例如受伤、攻击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;
if (this.hp < 0) this.hp = 0;
this.healthBar.set(this.hp);
// 先用drawAnimate凑活一下等下个版本提供更好的 api
if (this.stage === TowerBossStage.Stage3) {
core.drawAnimate('hand', 7, 2);
this.pop.addPop((-damage).toString(), 1000, 240, 80, '#dafc1d');
} else if (this.stage === TowerBossStage.Stage4) {
core.drawAnimate('hand', 7, 3);
this.pop.addPop((-damage).toString(), 1000, 240, 112, '#dafc1d');
} else if (this.stage === TowerBossStage.Stage5) {
core.drawAnimate('hand', 7, 4);
this.pop.addPop((-damage).toString(), 1000, 240, 144, '#dafc1d');
} else {
core.drawAnimate('hand', 7, 1);
this.pop.addPop((-damage).toString(), 1000, 240, 172, '#dafc1d');
}
}
/**
* 添加攻击boss的圆圈
* @param last 持续时长
* @param damage 造成的伤害
*/
addAttackCircle(_: number, n: number) {
const s = 13 - n * 2;
const nx = Math.floor(Math.random() * s + n + 1);
const ny = Math.floor(Math.random() * s + n + 1);
const proj = this.createProjectile(AttackProjectile, nx * 32, ny * 32);
proj.damage = 250 + Math.floor(Math.random() * 500);
}
ai(time: number, frame: number): void {
this.time = time;
const fixedTime = time - this.stageStartTime;
this.main.update();
this.check(time);
TowerBoss.effect.requestUpdate();
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.Stage4:
this.aiStage4(fixedTime, frame);
break;
case TowerBossStage.Stage5:
this.aiStage5(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: 开始血条动画
if (this.stageProgress === 0) {
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;
core.playBgm('towerBoss.opus');
}
}
async releaseSkill1() {
const locs = new Set<number>();
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);
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() * 11 + 2);
const y = Math.floor(Math.random() * 11 + 2);
const proj = this.createProjectile(PortalProjectile, 0, 0);
proj.setTarget(x, y);
proj.createEffect(TowerBoss.effect);
}
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:
// 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(500, 0);
this.attackTime++;
}
if (this.hp <= 7000) {
this.changeStage(TowerBossStage.Dialogue1, time);
this.attackTime = 1;
}
}
private aiDialogue1(time: number, _frame: number) {
this.changeStage(TowerBossStage.Stage2, time);
this.attackTime = 3;
this.skill4Time = 5;
this.skill5Time = 3;
core.playBgm('towerBoss2.opus');
const weather = WeatherController.get('main');
weather?.activate('rain', 6);
}
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);
}
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(500, 0);
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);
core.setBgFgBlock('bg', 0, nx, n - 1);
core.setBgFgBlock('bg', 0, nx, 15 - n);
}
for (let ny = n; ny < 15 - n; ny++) {
core.removeBlock(n - 1, ny);
core.removeBlock(15 - n, ny);
core.setBgFgBlock('bg', 0, n - 1, ny);
core.setBgFgBlock('bg', 0, 15 - n, ny);
}
for (let nx = n; nx < 15 - n; nx++) {
core.setBlock(527, nx, n);
core.setBlock(527, nx, 15 - n - 1);
}
for (let ny = n + 1; ny < 15 - n - 1; ny++) {
core.setBlock(527, n, ny);
core.setBlock(527, 15 - n - 1, ny);
}
core.stopAutomaticRoute();
core.setHeroLoc('x', 7);
core.setHeroLoc('y', 7);
core.setHeroLoc('direction', 'up');
core.setBlock(557, 7, n + 1);
}
private aiDialogue2(time: number, _frame: number) {
this.changeStage(TowerBossStage.Stage3, time);
this.attackTime = 3;
this.terrainClose(1);
this.skill6Time = 30;
this.skill7Time = 2;
core.playBgm('towerBoss3.opus');
}
releaseSkill6(n: number, last: number) {
const s = 15 - n * 2;
const x = Math.floor(Math.random() * s + n);
const y = Math.floor(Math.random() * s + n);
const proj = this.createProjectile(BoomProjectile, 0, 0);
proj.setData(x, y, last);
}
async releaseSkill7(n: number) {
const count = Math.floor(Math.random() * 6 + 3);
const nodes: LocArr[] = [];
let lastX = -1;
let lastY = -1;
const s = 15 - n * 2;
const used = new Set<number>();
let i = 0;
while (i < count) {
const x = Math.floor(Math.random() * s + n);
const y = Math.floor(Math.random() * s + n);
const index = x + y * s;
if (used.has(index)) continue;
i++;
used.add(index);
nodes.push([x, y]);
if (lastX !== -1 && lastY !== -1) {
const proj = this.createProjectile(ChainProjectile, 0, 0);
proj.hitbox.setPoint1(lastX * 32 + 16, lastY * 32 + 16);
proj.hitbox.setPoint2(x * 32 + 16, y * 32 + 16);
}
lastX = x;
lastY = y;
}
}
private aiStage3(time: number, _frame: number) {
const skill6Release = this.skill6Time * this.skill6Interval;
const skill7Release = this.skill7Time * this.skill7Interval;
const attack = this.attackTime * this.attackInterval;
if (time > skill6Release) {
this.releaseSkill6(2, 500);
this.skill6Time++;
}
if (time > skill7Release) {
this.releaseSkill7(2);
this.skill7Time++;
}
if (time > attack) {
this.addAttackCircle(500, 1);
this.attackTime++;
}
if (this.hp <= 2000) {
this.changeStage(TowerBossStage.Stage4, time);
this.terrainClose(2);
this.attackTime = 1;
this.skill6Time = 12;
this.skill6Interval = 400;
this.skill7Time = 1;
}
}
private aiStage4(time: number, _frame: number) {
const skill6Release = this.skill6Time * this.skill6Interval;
const skill7Release = this.skill7Time * this.skill7Interval;
const attack = this.attackTime * this.attackInterval;
if (time > skill6Release) {
this.releaseSkill6(3, 500);
this.skill6Time++;
}
if (time > skill7Release) {
this.releaseSkill7(3);
this.skill7Time++;
}
if (time > attack) {
this.addAttackCircle(500, 2);
this.attackTime++;
}
if (this.hp <= 1000) {
this.changeStage(TowerBossStage.Stage5, time);
this.terrainClose(3);
this.attackTime = 1;
this.skill6Time = 17;
this.skill6Interval = 300;
this.skill7Time = 1;
}
}
private aiStage5(time: number, _frame: number) {
const skill6Release = this.skill6Time * this.skill6Interval;
const skill7Release = this.skill7Time * this.skill7Interval;
const attack = this.attackTime * this.attackInterval;
if (time > skill6Release) {
this.releaseSkill6(4, 500);
this.skill6Time++;
}
if (time > skill7Release) {
this.releaseSkill7(4);
this.skill7Time++;
}
if (time > attack) {
this.addAttackCircle(500, 3);
this.attackTime++;
}
if (this.hp <= 0) {
this.changeStage(TowerBossStage.End, time);
}
}
private aiEnd(_time: number, _frame: number) {
this.end();
core.insertAction([
{ type: 'openDoor', loc: [13, 6], floorId: 'MT19' },
{ type: 'setValue', name: 'flag:boss1', value: 'true' },
{ type: 'changeFloor', floorId: 'MT20', loc: [7, 9] },
{ type: 'forbidSave' },
{ type: 'showStatusBar' }
]);
}
}
interface TextRenderable {
x: number;
y: number;
blur: number;
text: string;
}
class Word extends RenderItem {
private ani: Animation = new Animation();
/** 当前正在显示的文字 */
private showing: string = '';
/** 文字显示时间间隔 */
private showInterval: number = 100;
/** 文字显示的虚化时长 */
private showBlurTime: number = 200;
/** 显示开始时刻 */
private showStartTime: number = 0;
/** 最大虚化程度 */
private readonly MAX_BLUR = 5;
/**
* 初始化
*/
init() {
this.size(480, 24);
this.setHD(true);
this.setZIndex(95);
}
/**
* 降下背景
*/
curtainDown() {
this.delegateTicker(() => {
this.pos(this.ani.x, this.ani.y);
}, 700);
this.ani.time(600).mode(hyper('sin', 'out')).absolute().move(0, 24);
return sleep(700);
}
/**
* 升起背景
*/
curtainUp() {
this.delegateTicker(() => {
this.pos(this.ani.x, this.ani.y);
}, 700);
this.ani.time(600).mode(hyper('sin', 'out')).absolute().move(0, 0);
return sleep(700);
}
/**
* 显示文字,会将之前的文字取消显示
* @param text 要显示的文字
*/
showText(text: string) {
this.showStartTime = Date.now();
this.showing = text;
}
/**
* 设置文字显示的参数
* @param interval 文字显示时间间隔
* @param blurTime 文字显示虚化时长
*/
setParam(interval: number, blurTime: number) {
this.showInterval = interval;
this.showBlurTime = blurTime;
}
private getTextRenerable() {
const dt = Date.now() - this.showStartTime;
const res: TextRenderable[] = [];
[...this.showing].forEach((v, i) => {
const showStartTime = i * this.showInterval;
const blurRatio = (dt - showStartTime) / this.showBlurTime;
let blur = blurRatio * this.MAX_BLUR;
if (blur < 0) blur = 0;
else if (blur > this.MAX_BLUR) blur = this.MAX_BLUR;
const obj: TextRenderable = {
blur,
x: i * 18,
y: 12,
text: v
};
res.push(obj);
});
return res;
}
protected render(canvas: MotaOffscreenCanvas2D): void {
const data = this.getTextRenerable();
const ctx = canvas.ctx;
ctx.font = '18px "normal"';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
for (const { blur, x, y, text } of data) {
if (blur !== 0) {
ctx.filter = `blur(${blur}px)`;
} else {
ctx.filter = 'none';
}
ctx.fillText(text, x, y);
}
}
}
class HealthBar extends RenderItem {
private trans: Transition = new Transition();
/** 当前血条状态 */
private status: HealthBarStatus = HealthBarStatus.Start;
/**
* 初始化
*/
init() {
this.trans.time(2000).absolute().mode(power(3, 'out'));
this.trans.value.hp = 0;
this.trans.value.x = 0;
this.trans.value.y = -16;
this.size(480, 16);
this.setHD(true);
this.setZIndex(100);
}
/**
* 设置剩余血量
*/
set(value: number) {
this.trans.time(2000).mode(power(3, 'out')).transition('hp', value);
this.delegateTicker(() => {
this.update();
}, 2500);
}
/**
* 展示开始动画
*/
async showStart() {
if (this.status !== HealthBarStatus.Start) return;
this.delegateTicker(() => {
this.update();
}, 2500);
this.trans
.time(600)
.mode(hyper('sin', 'out'))
.absolute()
.transition('y', 0);
this.trans.time(2000).mode(power(3, 'out')).transition('hp', 10000);
await sleep(1700);
this.status = HealthBarStatus.Running;
}
/**
* 展示结束动画
*/
async showEnd() {
if (this.status !== HealthBarStatus.Running) return;
this.delegateTicker(() => {
this.update();
}, 2500);
this.trans
.time(600)
.mode(hyper('sin', 'in'))
.absolute()
.transition('y', -16);
await sleep(700);
this.status = HealthBarStatus.End;
}
protected render(canvas: MotaOffscreenCanvas2D): void {
const ctx = canvas.ctx;
const hp = this.trans.value.hp;
const ratio = hp / 10000;
const r = Math.min(255 * 2 - ratio * 2 * 255, 255);
const g = Math.min(ratio * 2 * 255, 255);
ctx.save();
ctx.translate(this.trans.value.x, this.trans.value.y);
ctx.fillStyle = '#bbb';
ctx.fillRect(2, 2, 480 - 4, 16 - 4);
const color = `rgb(${Math.floor(r)},${Math.floor(g)},0)`;
ctx.fillStyle = color;
ctx.fillRect(2, 2, (480 - 4) * ratio, 16 - 4);
ctx.font = '12px "normal"';
ctx.textBaseline = 'middle';
ctx.textAlign = 'right';
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.strokeText(`${Math.floor(hp)} / 10000`, 472, 8);
ctx.fillText(`${Math.floor(hp)} / 10000`, 472, 8);
ctx.lineWidth = 4;
ctx.strokeStyle = '#fff';
ctx.shadowBlur = 4;
ctx.shadowColor = '#000';
ctx.strokeRect(0, 0, 480, 16);
ctx.restore();
}
}