From 0604c56e980c9d3846227ce53d6e9291c852f3fd Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Tue, 1 Aug 2023 11:09:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=80=AA=E7=89=A9=E7=A2=8E=E8=A3=82=E7=89=B9?= =?UTF-8?q?=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/initPlugin.ts | 4 +- src/plugin/fx/frag.ts | 186 ++++++++++++++++++++++++++++++++ src/plugin/game/enemy/battle.ts | 30 +++++- src/plugin/ui/book.tsx | 9 -- src/plugin/ui/fixed.ts | 13 ++- src/types/plugin.d.ts | 10 ++ src/ui/book.vue | 11 +- 7 files changed, 247 insertions(+), 16 deletions(-) create mode 100644 src/plugin/fx/frag.ts diff --git a/src/initPlugin.ts b/src/initPlugin.ts index e5d3c91..ff218a1 100644 --- a/src/initPlugin.ts +++ b/src/initPlugin.ts @@ -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上 diff --git a/src/plugin/fx/frag.ts b/src/plugin/fx/frag.ts new file mode 100644 index 0000000..f7d6b68 --- /dev/null +++ b/src/plugin/fx/frag.ts @@ -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; +} diff --git a/src/plugin/game/enemy/battle.ts b/src/plugin/game/enemy/battle.ts index 933f7e6..fc97243 100644 --- a/src/plugin/game/enemy/battle.ts +++ b/src/plugin/game/enemy/battle.ts @@ -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 = []; diff --git a/src/plugin/ui/book.tsx b/src/plugin/ui/book.tsx index b9c38eb..57ee632 100644 --- a/src/plugin/ui/book.tsx +++ b/src/plugin/ui/book.tsx @@ -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 怪物实例 diff --git a/src/plugin/ui/fixed.ts b/src/plugin/ui/fixed.ts index 4532063..bc96290 100644 --- a/src/plugin/ui/fixed.ts +++ b/src/plugin/ui/fixed.ts @@ -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; diff --git a/src/types/plugin.d.ts b/src/types/plugin.d.ts index 4b426cb..4e87073 100644 --- a/src/types/plugin.d.ts +++ b/src/types/plugin.d.ts @@ -99,6 +99,16 @@ interface PluginDeclaration * @param value 要判断的值 */ has(value: T): value is NonNullable; + + applyFragWith( + canvas: HTMLCanvasElement, + length?: number, + time?: number + ): { + canvas: HTMLCanvasElement; + onEnd: Promise; + animation: import('mutate-animate').Animation; + }; } interface GamePluginUtils { diff --git a/src/ui/book.vue b/src/ui/book.vue index 9f8061b..be67333 100644 --- a/src/ui/book.vue +++ b/src/ui/book.vue @@ -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];