怪物碎裂特效

This commit is contained in:
unanmed 2023-08-01 11:09:23 +08:00
parent 62e8f91436
commit 0604c56e98
7 changed files with 247 additions and 16 deletions

View File

@ -19,6 +19,7 @@ import path from './plugin/fx/path';
import gameCanvas from './plugin/fx/gameCanvas';
import noise from './plugin/fx/noise';
import smooth from './plugin/fx/smoothView';
import frag from './plugin/fx/frag';
function forward() {
const toForward: any[] = [
@ -42,7 +43,8 @@ function forward() {
path(),
gameCanvas(),
noise(),
smooth()
smooth(),
frag()
];
// 初始化所有插件并转发到core上

186
src/plugin/fx/frag.ts Normal file
View File

@ -0,0 +1,186 @@
import { Animation, bezier, sleep } from 'mutate-animate';
interface SplittedImage {
canvas: HTMLCanvasElement;
x: number;
y: number;
}
interface FraggingImage extends SplittedImage {
/** 横坐标增量 */
deltaX: number;
/** 纵坐标增量 */
deltaY: number;
endRad: number;
}
/** 最大移动距离,最终位置距离中心的距离变成原来的几倍 */
const MAX_MOVE_LENGTH = 1.15;
/** 移动距离波动,在最大移动距离的基础上加上多少倍距离的波动距离 */
const MOVE_FLUSH = 0.2;
/** 最大旋转角,单位是弧度 */
const MAX_ROTATE = 0.4;
/** 碎裂动画的速率曲线函数 */
const FRAG_TIMING = bezier(0.75);
export default function init() {
return { applyFragWith };
}
export function applyFragWith(
canvas: HTMLCanvasElement,
length: number = 4,
time: number = 4000
) {
// 先切分图片
const imgs = splitCanvas(canvas, length);
const cx = canvas.width / 2;
const cy = canvas.height / 2;
let maxX = 0;
let maxY = 0;
const toMove: FraggingImage[] = imgs.map(v => {
const centerX = v.x + v.canvas.width / 2;
const centerY = v.y + v.canvas.height / 2;
const onX = centerX === cx;
const onY = centerY === cy;
const rate = MAX_MOVE_LENGTH - 1 + Math.random() * MOVE_FLUSH;
let endX = onY ? 0 : (centerX - cx) * rate;
let endY = onX ? 0 : (centerY - cy) * rate;
const mx = Math.abs(endX + centerX) + Math.abs(v.canvas.width);
const my = Math.abs(endY + centerY) + Math.abs(v.canvas.height);
if (mx > maxX) maxX = mx;
if (my > maxY) maxY = my;
const endRad = Math.random() * MAX_ROTATE * 2 - MAX_ROTATE;
return {
deltaX: endX,
deltaY: endY,
endRad,
x: centerX,
y: centerY,
canvas: v.canvas
};
});
// 再执行动画
const frag = document.createElement('canvas');
const ctx = frag.getContext('2d')!;
const ani = new Animation();
ani.register('rate', 0);
ani.absolute().time(time).mode(FRAG_TIMING).apply('rate', 1);
frag.width = maxX * 2;
frag.height = maxY * 2;
ctx.save();
const dw = maxX - canvas.width / 2;
const dh = maxY - canvas.height / 2;
const fragFn = () => {
const rate = ani.value.rate;
const opacity = 1 - rate;
ctx.globalAlpha = opacity;
ctx.clearRect(0, 0, frag.width, frag.height);
toMove.forEach(v => {
ctx.save();
const nx = v.deltaX * rate;
const ny = v.deltaY * rate;
const rotate = v.endRad * rate;
ctx.translate(nx + v.x + dw, ny + v.y + dh);
ctx.rotate(rotate);
ctx.drawImage(
v.canvas,
nx - v.canvas.width / 2,
ny - v.canvas.height / 2
);
ctx.restore();
});
};
const onEnd = () => {};
ani.ticker.add(fragFn);
return makeFragManager(frag, ani, time, onEnd);
}
function makeFragManager(
canvas: HTMLCanvasElement,
ani: Animation,
time: number,
onEnd: () => void
) {
const promise = sleep(time + 50);
return {
animation: ani,
onEnd: promise.then(() => {
ani.ticker.destroy();
onEnd();
}),
canvas
};
}
export function withImage(
image: CanvasImageSource,
sx: number,
sy: number,
sw: number,
sh: number
): SplittedImage {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = sw;
canvas.height = sh;
ctx.drawImage(image, sx, sy, sw, sh, 0, 0, sw, sh);
return { canvas, x: sx, y: sy };
}
/**
*
* @param canvas
* @param l
*/
function splitCanvas(canvas: HTMLCanvasElement, l: number): SplittedImage[] {
if (canvas.width / l < 2 || canvas.height / l < 2) {
console.warn('切分画布要求切分边长大于等于画布长宽的一半!');
return [];
}
const w = canvas.width % l === 0 ? canvas.width : canvas.width - l;
const h = canvas.height % l === 0 ? canvas.height : canvas.height - l;
const numX = Math.floor(w / l);
const numY = Math.floor(h / l);
const rw = (w - numX * l) / 2;
const rh = (h - numY * l) / 2;
const res: SplittedImage[] = [];
if (rw > 0) {
if (rh > 0) {
res.push(
withImage(canvas, 0, 0, rw, rh),
withImage(canvas, 0, canvas.height - rh, rw, rh),
withImage(canvas, canvas.width - rw, 0, rw, rh),
withImage(canvas, canvas.width - rw, canvas.height - rh, rw, rh)
);
}
for (const x of [0, canvas.width - rw]) {
for (let ny = 0; ny < numY; ny++) {
res.push(withImage(canvas, x, rh + l * ny, rw, l));
}
}
}
if (rh > 0) {
for (const y of [0, canvas.height - rh]) {
for (let nx = 0; nx < numX; nx++) {
res.push(withImage(canvas, rw + l * nx, y, l, rh));
}
}
}
for (let nx = 0; nx < numX; nx++) {
for (let ny = 0; ny < numY; ny++) {
res.push(withImage(canvas, rw + l * nx, rh + l * ny, l, l));
}
}
return res;
}

View File

@ -183,11 +183,37 @@ core.events.afterBattle = function (
// 如果已有事件正在处理中
if (core.status.event.id == null) core.continueAutomaticRoute();
else core.clearContinueAutomaticRoute();
// 打怪特效
if (has(x) && has(y)) {
const frame = core.status.globalAnimateStatus % 2;
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;
core.drawIcon(canvas, enemy.id, 0, 0, 32, 32, frame);
const manager = core.applyFragWith(canvas);
const frag = manager.canvas;
frag.style.imageRendering = 'pixelated';
frag.style.width = `${frag.width * core.domStyle.scale}px`;
frag.style.height = `${frag.height * core.domStyle.scale}px`;
const left =
(x * 32 + 16 - frag.width / 2 - core.bigmap.offsetX) *
core.domStyle.scale;
const top =
(y * 32 + 16 - frag.height / 2 - core.bigmap.offsetY) *
core.domStyle.scale;
frag.style.left = `${left}px`;
frag.style.top = `${top}px`;
frag.style.zIndex = '45';
frag.style.position = 'absolute';
core.dom.gameDraw.appendChild(frag);
manager.onEnd.then(() => {
frag.remove();
});
}
};
core.events._sys_battle = function (data: Block, callback?: () => void) {
// todo: 重写这个函数的一部分
// 检查战前事件
const floor = core.floors[core.status.floorId];
const beforeBattle: MotaEvent = [];

View File

@ -21,15 +21,6 @@ interface BookDetailInfo {
export const detailInfo: BookDetailInfo = {};
export const specials = Object.fromEntries(
core.getSpecials().map(v => {
return [v[0], v.slice(1)];
})
) as Record<
string,
EnemySpecialDeclaration extends [number, ...infer F] ? F : never
>;
/**
*
* @param enemy

View File

@ -1,7 +1,7 @@
import { cloneDeep, debounce } from 'lodash-es';
import { ref } from 'vue';
import { getDamageColor } from '../utils';
import { ToShowEnemy, detailInfo, specials } from './book';
import { ToShowEnemy, detailInfo } from './book';
import { DamageEnemy } from '../game/enemy/damage';
export const showFixed = ref(false);
@ -54,8 +54,15 @@ export function getDetailedEnemy(
enemy: DamageEnemy,
floorId: FloorIds = core.status.floorId
): ToShowEnemy {
// todo: 删除 getDamageInfo
// todo: 不使用 nextCriticals
const specials = Object.fromEntries(
core.getSpecials().map(v => {
return [v[0], v.slice(1)];
})
) as Record<
string,
EnemySpecialDeclaration extends [number, ...infer F] ? F : never
>;
const ratio = core.status.maps[floorId].ratio;
const dam = enemy.calEnemyDamage(core.status.hero, 'none')[0].damage;

10
src/types/plugin.d.ts vendored
View File

@ -99,6 +99,16 @@ interface PluginDeclaration
* @param value
*/
has<T>(value: T): value is NonNullable<T>;
applyFragWith(
canvas: HTMLCanvasElement,
length?: number,
time?: number
): {
canvas: HTMLCanvasElement;
onEnd: Promise<void>;
animation: import('mutate-animate').Animation;
};
}
interface GamePluginUtils {

View File

@ -47,12 +47,21 @@ import BookDetail from './bookDetail.vue';
import { LeftOutlined } from '@ant-design/icons-vue';
import { KeyCode } from '../plugin/keyCodes';
import { noClosePanel } from '../plugin/uiController';
import { ToShowEnemy, detailInfo, specials } from '../plugin/ui/book';
import { ToShowEnemy, detailInfo } from '../plugin/ui/book';
const floorId =
// @ts-ignore
core.floorIds[core.status.event?.ui?.index] ?? core.status.floorId;
const specials = Object.fromEntries(
core.getSpecials().map(v => {
return [v[0], v.slice(1)];
})
) as Record<
string,
EnemySpecialDeclaration extends [number, ...infer F] ? F : never
>;
const enemy = core.getCurrentEnemys(floorId);
const toShow: ToShowEnemy[] = enemy.map(v => {
const cri = v.enemy.calCritical(1, 'none')[0];