Compare commits

...

2 Commits

Author SHA1 Message Date
4102893916 feat: 第二章特殊战第一种弹幕 2024-12-24 18:11:18 +08:00
44276183bb feat: ui控制器接口设计 2024-12-24 18:10:49 +08:00
8 changed files with 470 additions and 4 deletions

1
src/core/system/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './ui';

View File

@ -0,0 +1,55 @@
import { Component, VNodeProps } from 'vue';
export interface IUIControllerConfig<Element, UI> {
/**
* ui挂载至目标元素时的操作
* @param element
* @param ui ui对象
*/
insert(element: Element, ui: UI): void;
/**
* ui从目标元素上移除时的操作
* @param element ui的父元素
* @param ui ui元素
*/
remove(element: Element, ui: UI): void;
/**
* UI
* @param component UI组件
* @param props UI传递的props
*/
createUI(
component: Component,
props?: (VNodeProps & { [key: string]: any }) | null
): UI;
}
export const enum OpenOption {
Push,
Unshift
}
export const enum CloseOption {
Splice,
Pop,
Shift
}
export class UIController<Element, UI> {
constructor(config: IUIControllerConfig<Element, UI>) {}
/**
* ui改变时控制器的行为
* @param open
* @param close
*/
setChangeMode(open: OpenOption, close: CloseOption) {}
/**
* UI控制器挂载至容器上
* @param container
*/
mount(container: Element) {}
}

View File

@ -0,0 +1 @@
export * from './controller';

View File

@ -17,8 +17,11 @@ export abstract class BarrageBoss extends EventEmitter<BarrageBossEvent> {
/** 开始时刻 */
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<BarrageBossEvent> {
* 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<BarrageBossEvent> {
v.doDamage(this.state);
}
});
this.lastTime = now;
};
/**
@ -230,8 +236,9 @@ export abstract class Projectile<T extends BarrageBoss = BarrageBoss> {
* @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的弹幕应该全部画在同一层

View File

@ -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<BarrageBoss>;
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<PalaceBoss> {
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 {}
}

View File

@ -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<PalaceBoss> {
damage: number = 10000;
hitbox: Hitbox.Circle = new Hitbox.Circle(0, 0, 8);
static ball: Map<string, MotaOffscreenCanvas2D> = 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<string, string[]>) {
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);
}
}

View File

@ -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;

View File

@ -517,3 +517,11 @@ export function calStringSize(str: string) {
return size;
}
export function clamp(num: number, start: number, end: number) {
const s = Math.min(start, end);
const e = Math.max(start, end);
if (num < s) return s;
else if (num > e) return e;
return num;
}