HumanBreak/packages/legacy-ui/src/utils.ts

394 lines
10 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 { isNil } from 'lodash-es';
import { Animation, sleep, TimingFn } from 'mutate-animate';
import { Ref, ref } from 'vue';
import axios from 'axios';
import { decompressFromBase64 } from 'lz-string';
import { logger } from '@motajs/common';
type CanParseCss = keyof {
[P in keyof CSSStyleDeclaration as CSSStyleDeclaration[P] extends string
? P extends string
? P
: never
: never]: CSSStyleDeclaration[P];
};
/**
* 根据伤害大小获取颜色
* @param damage 伤害大小
*/
export function getDamageColor(damage: number): string {
if (typeof damage !== 'number') return '#f00';
if (damage === 0) return '#2f2';
if (damage < 0) return '#7f7';
if (damage < core.status.hero.hp / 3) return '#fff';
if (damage < (core.status.hero.hp * 2) / 3) return '#ff4';
if (damage < core.status.hero.hp) return '#f93';
return '#f22';
}
/**
* 设置画布的长宽
* @param canvas 画布
* @param w 宽度
* @param h 高度
*/
export function setCanvasSize(
canvas: HTMLCanvasElement,
w: number,
h: number
): void {
canvas.width = w;
canvas.height = h;
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
}
/**
* 解析css字符串为CSSStyleDeclaration对象
* @param css 要解析的css字符串
*/
export function parseCss(css: string): Partial<Record<CanParseCss, string>> {
if (css.length === 0) return {};
let pointer = -1;
let inProp = true;
let prop = '';
let value = '';
let upper = false;
const res: Partial<Record<CanParseCss, string>> = {};
while (++pointer < css.length) {
const char = css[pointer];
if ((char === ' ' || char === '\n' || char === '\r') && inProp) {
continue;
}
if (char === '-' && inProp) {
if (prop.length !== 0) {
upper = true;
}
continue;
}
if (char === ':') {
if (!inProp) {
logger.error(3, pointer.toString(), css);
return res;
}
inProp = false;
continue;
}
if (char === ';') {
if (prop.length === 0) continue;
if (inProp) {
logger.error(4, pointer.toString(), css);
return res;
}
res[prop as CanParseCss] = value.trim();
inProp = true;
prop = '';
value = '';
continue;
}
if (upper) {
if (!inProp) {
logger.error(5, pointer.toString(), css);
}
prop += char.toUpperCase();
upper = false;
} else {
if (inProp) prop += char;
else value += char;
}
}
if (inProp && prop.length > 0) {
logger.error(6, pointer.toString(), css);
return res;
}
if (!inProp && value.trim().length === 0) {
logger.error(7, pointer.toString(), css);
return res;
}
if (prop.length > 0) res[prop as CanParseCss] = value.trim();
return res;
}
export function stringifyCSS(css: Partial<Record<CanParseCss, string>>) {
let str = '';
for (const [key, value] of Object.entries(css)) {
let pointer = -1;
let prop = '';
while (++pointer < key.length) {
const char = key[pointer];
if (char.toLowerCase() === char) {
prop += char;
} else {
prop += `-${char.toLowerCase()}`;
}
}
str += `${prop}:${value};`;
}
return str;
}
/**
* 使用打字机效果显示一段文字
* @param str 要打出的字符串
* @param time 打出总用时默认1秒当第四个参数是true时该项为每个字的平均时间
* @param timing 打出时的速率曲线,默认为线性变化
* @param avr 是否将第二个参数视为每个字的平均时间
* @returns 文字的响应式变量
*/
export function type(
str: string,
time: number = 1000,
timing: TimingFn = n => n,
avr: boolean = false
): Ref<string> {
const toShow = eval('`' + str + '`') as string;
if (typeof toShow !== 'string') {
throw new TypeError('Error str type in typing!');
}
if (toShow.startsWith('!!html')) return ref(toShow);
if (avr) time *= toShow.length;
const ani = new Animation();
const content = ref('');
const all = toShow.length;
const fn = (time: number) => {
if (isNil(time)) return;
const now = ani.x;
content.value = toShow.slice(0, Math.floor(now));
if (Math.floor(now) === all) {
ani.ticker.destroy();
content.value = toShow;
}
};
ani.ticker.add(fn);
ani.mode(timing).time(time).move(all, 0);
setTimeout(() => ani.ticker.destroy(), time + 100);
return content;
}
/**
* 设置文字分段换行等
* @param str 文字
*/
export function splitText(str: string[]) {
return str
.map((v, i, a) => {
if (/^\d+\./.test(v)) return `${'&nbsp;'.repeat(12)}${v}`;
else if (
(!isNil(a[i - 1]) && v !== '<br>' && a[i - 1] === '<br>') ||
i === 0
) {
return `${'&nbsp;'.repeat(8)}${v}`;
} else return v;
})
.join('');
}
/**
* 在下一帧执行某个函数
* @param cb 执行的函数
*/
export function nextFrame(cb: (time: number) => void) {
requestAnimationFrame(() => {
requestAnimationFrame(cb);
});
}
/**
* 下载一个画布对应的图片
* @param canvas 画布
* @param name 图片名称
*/
export function downloadCanvasImage(
canvas: HTMLCanvasElement,
name: string
): void {
const data = canvas.toDataURL('image/png');
download(data, name);
}
/**
* 下载一个文件
* @param content 下载的内容
* @param name 文件名称
*/
export function download(content: string, name: string) {
const a = document.createElement('a');
a.download = `${name}.png`;
a.href = content;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
/**
* 间隔一段时间调用一个函数
* @param funcs 函数列表
* @param interval 调用间隔
*/
export async function doByInterval(
funcs: (() => void)[],
interval: number,
awaitFirst: boolean = false
) {
if (awaitFirst) {
await sleep(interval);
}
for await (const fn of funcs) {
fn();
await sleep(interval);
}
}
/**
* 更改一个本地存储
* @deprecated
* @param name 要更改的信息
* @param fn 更改时执行的函数
* @param defaultValue 如果不存在时获取的默认值
*/
export function changeLocalStorage<T>(
name: string,
fn: (data: T) => T,
defaultValue?: T
) {
const now = core.getLocalStorage(name, defaultValue);
const to = fn(now);
core.setLocalStorage(name, to);
}
export async function swapChapter(chapter: number, hard: number) {
const h = hard === 2 ? 'hard' : 'easy';
const save = await axios.get(
`${import.meta.env.BASE_URL}swap/${chapter}.${h}.h5save`,
{
responseType: 'text',
responseEncoding: 'utf-8'
}
);
const data = JSON.parse(decompressFromBase64(save.data));
core.loadData(data.data, () => {
core.removeFlag('__fromLoad__');
core.drawTip('读档成功');
});
}
export function ensureArray<T>(arr: T): T extends any[] ? T : T[] {
// @ts-expect-error 暂时无法推导
return arr instanceof Array ? arr : [arr];
}
export async function triggerFullscreen(full: boolean) {
const { maxGameScale } = Mota.require('@user/data-utils');
if (!!document.fullscreenElement && !full) {
if (window.jsinterface) {
window.jsinterface.requestPortrait();
return;
}
await document.exitFullscreen();
requestAnimationFrame(() => {
maxGameScale(1);
});
}
if (full && !document.fullscreenElement) {
if (window.jsinterface) {
window.jsinterface.requestLandscape();
return;
}
await document.body.requestFullscreen();
requestAnimationFrame(() => {
maxGameScale();
});
}
}
/**
* 获得某个状态的中文名
* @param name 要获取的属性名
*/
export function getStatusLabel(name: string) {
return (
{
name: '名称',
lv: '等级',
hpmax: '生命回复',
hp: '生命',
manamax: '魔力上限',
mana: '额外攻击',
atk: '攻击',
def: '防御',
mdef: '智慧',
money: '金币',
exp: '经验',
point: '加点',
steps: '步数',
up: '升级',
magicDef: '魔法防御',
none: '无'
}[name] || name
);
}
export function formatSize(size: number) {
return size < 1 << 10
? `${size.toFixed(2)}B`
: size < 1 << 20
? `${(size / (1 << 10)).toFixed(2)}KB`
: size < 1 << 30
? `${(size / (1 << 20)).toFixed(2)}MB`
: `${(size / (1 << 30)).toFixed(2)}GB`;
}
export function getIconHeight(icon: AllIds | 'hero') {
if (icon === 'hero') {
if (core.isPlaying()) {
return (
core.material.images.images[core.status.hero.image].height / 4
);
} else {
return 48;
}
}
return core.getBlockInfo(icon)?.height ?? 32;
}
const ascii = 16 ** 2;
const other = 16 ** 4;
const emoji = 16 ** 5;
export function calStringSize(str: string) {
let size = 0;
for (const char of str) {
const num = char.charCodeAt(0);
if (num < ascii) size += 1;
else if (num < other) size += 2;
else if (num < emoji) size += 2.5;
}
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;
}