mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-11-04 15:12:58 +08:00 
			
		
		
		
	怪物碎裂特效
This commit is contained in:
		
							parent
							
								
									62e8f91436
								
							
						
					
					
						commit
						0604c56e98
					
				@ -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
									
								
							
							
						
						
									
										186
									
								
								src/plugin/fx/frag.ts
									
									
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
@ -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 = [];
 | 
			
		||||
 | 
			
		||||
@ -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 怪物实例
 | 
			
		||||
 | 
			
		||||
@ -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
									
									
								
							
							
						
						
									
										10
									
								
								src/types/plugin.d.ts
									
									
									
									
										vendored
									
									
								
							@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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];
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user