refactor: 修复所有的循环引用

This commit is contained in:
unanmed 2025-03-09 19:15:22 +08:00
parent 4fda246a9a
commit 23a8c0bf3a
82 changed files with 1015 additions and 2087 deletions

View File

@ -11,13 +11,6 @@ import {
Font
} from '@motajs/render';
import { WeatherController } from '../../weather';
import {
FloorChange,
LayerGroupFilter,
LayerGroupHalo,
LayerGroupPortal,
PopText
} from '@user/legacy-plugin-client';
import { defineComponent, onMounted, reactive, ref } from 'vue';
import { Textbox, Tip } from '../components';
import { GameUI, UIController } from '@motajs/system-ui';
@ -40,6 +33,11 @@ import { jumpIgnoreFloor } from '@user/legacy-plugin-data';
import { hook } from '@user/data-base';
import { FloorDamageExtends } from '../damage';
import { FloorItemDetail } from '../itemDetail';
import { LayerGroupPortal } from '../legacy/portal';
import { LayerGroupFilter } from '../legacy/gameCanvas';
import { LayerGroupHalo } from '../legacy/halo';
import { FloorChange } from '../legacy/fallback';
import { PopText } from '../legacy/pop';
const MainScene = defineComponent(() => {
const layerGroupExtends: ILayerGroupRenderExtends[] = [

View File

@ -13,17 +13,14 @@ import {
StepForward,
ViewMapIcon
} from '../components/icons';
import {
generateBinary,
getVitualKeyOnce,
openDanmakuPoster
} from '@motajs/legacy-ui';
import { getVitualKeyOnce, openDanmakuPoster } from '@motajs/legacy-ui';
import { gameKey } from '@motajs/system-action';
import { generateKeyboardEvent } from '@motajs/system-action';
import { transitioned } from '../use';
import { linear } from 'mutate-animate';
import { KeyCode } from '@motajs/client-base';
import { Progress } from '../components/misc';
import { generateBinary } from '@motajs/legacy-common';
interface ToolbarProps extends DefaultProps {
loc?: ElementLocator;

View File

@ -2,7 +2,7 @@ import { getHeroStatusOf, getHeroStatusOn } from '../state/hero';
import { Range, ensureArray, has, manhattan } from '@user/data-utils';
import EventEmitter from 'eventemitter3';
import { hook } from '@user/data-base';
import { HeroSkill, NightSpecial } from '../mechanism/misc';
import { HeroSkill, NightSpecial } from '../mechanism';
import {
EnemyInfo,
DamageInfo,
@ -16,6 +16,7 @@ import {
HaloType,
IEnemyCollectionEvent
} from '@motajs/types';
import { isNil } from 'lodash-es';
// todo: 光环划分优先级,从而可以实现光环的多级运算
@ -293,7 +294,7 @@ export class DamageEnemy implements IDamageEnemy {
// 融化,融化不属于怪物光环,因此不能用provide和inject计算,需要在这里计算
const melt = flags[`melt_${floorId}`];
if (has(melt) && has(this.x) && has(this.y)) {
if (!isNil(melt) && !isNil(this.x) && !isNil(this.y)) {
for (const [loc, per] of Object.entries(melt)) {
const [mx, my] = loc.split(',').map(v => parseInt(v));
if (

View File

@ -1 +1,2 @@
export * from './misc';
export * from './skillTree';

View File

@ -48,131 +48,6 @@ export namespace NightSpecial {
}
}
export namespace HeroSkill {
export const enum Skill {
None,
/** 断灭之刃 */
Blade,
/** 铸剑为盾 */
Shield,
/** 跳跃 */
Jump
}
export const Blade = Skill.Blade;
export const Shield = Skill.Shield;
export const Jump = Skill.Jump;
const skillNameMap = new Map<Skill, string>([
[Skill.Blade, '断灭之刃'],
[Skill.Shield, '铸剑为盾'],
[Skill.Jump, '跳跃']
]);
const skillDesc = new Map<Skill, (level: number) => string>([
[
Skill.Blade,
level => `攻击上升 ${level * 10}%,防御下降 ${level * 10}%`
],
[
Skill.Shield,
level => `防御上升 ${level * 10}%,攻击下降 ${level * 10}%`
],
[Skill.Jump, () => `跳过前方障碍,或踢走面前的怪物`]
]);
interface SkillSave {
autoSkill: boolean;
learned: Skill[];
}
const learned = new Set<Skill>();
let autoSkill = true;
let enabled: Skill = Skill.None;
export function getLevel(skill: Skill = getEnabled()) {
switch (skill) {
case Blade:
return getSkillLevel(2);
case Jump:
return learned.has(Jump) ? 1 : 0;
case Shield:
return getSkillLevel(10);
}
return 0;
}
export function getSkillName(skill: Skill = getEnabled()) {
return skillNameMap.get(skill) ?? '未开启技能';
}
export function getSkillDesc(
skill: Skill = getEnabled(),
level: number = getLevel()
) {
return skillDesc.get(skill)?.(level) ?? '';
}
export function setAutoSkill(auto: boolean) {
autoSkill = auto;
}
export function getAutoSkill() {
return autoSkill;
}
export function learnedSkill(skill: Skill) {
return learned.has(skill);
}
export function learnSkill(skill: Skill) {
learned.add(skill);
}
export function forgetSkill(skill: Skill) {
learned.delete(skill);
}
export function clearSkill() {
learned.clear();
}
export function saveSkill(): SkillSave {
return { autoSkill, learned: [...learned] };
}
export function loadSkill(skills: SkillSave) {
learned.clear();
for (const skill of skills.learned) {
learned.add(skill);
}
autoSkill = skills.autoSkill;
}
export function getAll() {
return learned;
}
export function toggleSkill(skill: Skill) {
if (!learned.has(skill)) return;
if (enabled !== skill) enabled = skill;
else enabled = Skill.None;
}
export function enableSkill(skill: Skill) {
if (!learned.has(skill)) return;
enabled = skill;
}
export function disableSkill() {
enabled = Skill.None;
}
export function getEnabled() {
return enabled;
}
}
export namespace BluePalace {
type DoorConvertInfo = [id: AllIds, x: number, y: number];

View File

@ -1,5 +1,3 @@
import { HeroSkill } from './misc';
let levels: number[] = [];
export type Chapter = 'chapter1' | 'chapter2';
@ -314,3 +312,128 @@ export function saveSkillTree() {
export function loadSkillTree(data: number[]) {
levels = data ?? [];
}
export namespace HeroSkill {
export const enum Skill {
None,
/** 断灭之刃 */
Blade,
/** 铸剑为盾 */
Shield,
/** 跳跃 */
Jump
}
export const Blade = Skill.Blade;
export const Shield = Skill.Shield;
export const Jump = Skill.Jump;
const skillNameMap = new Map<Skill, string>([
[Skill.Blade, '断灭之刃'],
[Skill.Shield, '铸剑为盾'],
[Skill.Jump, '跳跃']
]);
const skillDesc = new Map<Skill, (level: number) => string>([
[
Skill.Blade,
level => `攻击上升 ${level * 10}%,防御下降 ${level * 10}%`
],
[
Skill.Shield,
level => `防御上升 ${level * 10}%,攻击下降 ${level * 10}%`
],
[Skill.Jump, () => `跳过前方障碍,或踢走面前的怪物`]
]);
interface SkillSave {
autoSkill: boolean;
learned: Skill[];
}
const learned = new Set<Skill>();
let autoSkill = true;
let enabled: Skill = Skill.None;
export function getLevel(skill: Skill = getEnabled()) {
switch (skill) {
case Blade:
return getSkillLevel(2);
case Jump:
return learned.has(Jump) ? 1 : 0;
case Shield:
return getSkillLevel(10);
}
return 0;
}
export function getSkillName(skill: Skill = getEnabled()) {
return skillNameMap.get(skill) ?? '未开启技能';
}
export function getSkillDesc(
skill: Skill = getEnabled(),
level: number = getLevel()
) {
return skillDesc.get(skill)?.(level) ?? '';
}
export function setAutoSkill(auto: boolean) {
autoSkill = auto;
}
export function getAutoSkill() {
return autoSkill;
}
export function learnedSkill(skill: Skill) {
return learned.has(skill);
}
export function learnSkill(skill: Skill) {
learned.add(skill);
}
export function forgetSkill(skill: Skill) {
learned.delete(skill);
}
export function clearSkill() {
learned.clear();
}
export function saveSkill(): SkillSave {
return { autoSkill, learned: [...learned] };
}
export function loadSkill(skills: SkillSave) {
learned.clear();
for (const skill of skills.learned) {
learned.add(skill);
}
autoSkill = skills.autoSkill;
}
export function getAll() {
return learned;
}
export function toggleSkill(skill: Skill) {
if (!learned.has(skill)) return;
if (enabled !== skill) enabled = skill;
else enabled = Skill.None;
}
export function enableSkill(skill: Skill) {
if (!learned.has(skill)) return;
enabled = skill;
}
export function disableSkill() {
enabled = Skill.None;
}
export function getEnabled() {
return enabled;
}
}

View File

@ -1,7 +1,7 @@
import { logger } from '@motajs/common';
import { EventEmitter } from 'eventemitter3';
import { cloneDeep } from 'lodash-es';
import { HeroSkill, NightSpecial } from '../mechanism/misc';
import { HeroSkill, NightSpecial } from '../mechanism';
/**
* 获取勇士在某一点的属性

View File

@ -9,7 +9,7 @@ import {
Transform,
MotaOffscreenCanvas2D
} from '@motajs/render';
import { Pop } from '../fx/pop';
import { Pop } from '../../../client-modules/src/render/legacy/pop';
import { SplittableBall } from './palaceBossProjectile';
import { PointEffect } from '../fx/pointShader';
import { loading } from '@user/data-base';

View File

@ -22,10 +22,10 @@ import {
ThunderProjectile
} from './towerBossProjectile';
import { IStateDamageable } from '@user/data-state';
import { Pop } from '../fx/pop';
import { WeatherController } from '@user/client-modules';
import { Pop } from '../../../client-modules/src/render/legacy/pop';
import { loading } from '@user/data-base';
import { clip } from '@user/legacy-plugin-data';
import { WeatherController } from '@user/client-modules';
loading.once('coreInit', () => {
const shader = new Shader();

View File

@ -1,6 +1,6 @@
import { Animation, hyper, linear, power, sleep } from 'mutate-animate';
import { Chase, ChaseData, IChaseController } from './chase';
import { completeAchievement } from '@motajs/legacy-ui';
// import { completeAchievement } from '@motajs/legacy-ui';
import {
Camera,
CameraAnimation,
@ -206,7 +206,7 @@ export function initChase(): IChaseController {
core.removeFlag('chaseId');
if (success) {
completeAchievement('challenge', 0);
// completeAchievement('challenge', 0);
}
});

View File

@ -1,5 +1 @@
export * from './gameCanvas';
export * from './halo';
export * from './pointShader';
export * from './pop';
export * from './portal';

View File

@ -1,6 +1,3 @@
export * from './boss';
export * from './chase';
export * from './fx';
export * from './fallback';
export * from '../../client-modules/src/render/loopMap';

View File

@ -21,5 +21,4 @@ export * from './removeMap';
export * from './replay';
export * from './shop';
export * from './skill';
export * from '../../data-state/src/mechanism/skillTree';
export * from './ui';

View File

@ -1,6 +1,6 @@
// @ts-nocheck
import { HeroSkill } from '@/game/mechanism/misc';
import { HeroSkill } from '@user/data-state';
// 所有的主动技能效果
var ignoreInJump = {

View File

@ -2,3 +2,4 @@ export * from './patch';
export * from './disposable';
export * from './eventEmitter';
export * from './resource';
export * from './utils';

View File

@ -0,0 +1,48 @@
import { EVENT_KEY_CODE_MAP } from '@motajs/client-base';
export function flipBinary(num: number, col: number) {
const n = 1 << col;
if (num & n) return num & ~n;
else return num | n;
}
/**
* 根据布尔值数组转换成一个二进制数
* @param arr 要转换的布尔值数组
*/
export function generateBinary(arr: boolean[]) {
let num = 0;
arr.forEach((v, i) => {
if (v) {
num |= 1 << i;
}
});
return num;
}
/**
* 删除数组内的某个项,返回删除后的数组
* @param arr 要操作的数组
* @param ele 要删除的项
*/
export function deleteWith<T>(arr: T[], ele: T): T[] {
const index = arr.indexOf(ele);
if (index === -1) return arr;
arr.splice(index, 1);
return arr;
}
export function spliceBy<T>(arr: T[], from: T): T[] {
const index = arr.indexOf(from);
if (index === -1) return arr;
arr.splice(index);
return arr;
}
/**
* 获取事件中的keycode对应的键
* @param key 要获取的键
*/
export function keycode(key: number) {
return EVENT_KEY_CODE_MAP[key];
}

View File

@ -49,8 +49,13 @@
<script lang="ts" setup>
import { onMounted, onUnmounted, onUpdated, ref, watch } from 'vue';
import { ArrowsAltOutlined, DragOutlined } from '@ant-design/icons-vue';
import { isMobile, useDrag, cancelGlobalDrag } from '../use';
import { has, requireUniqueSymbol } from '../utils';
import {
isMobile,
useDrag,
cancelGlobalDrag,
requireUniqueSymbol
} from '../use';
import { isNil } from 'lodash-es';
// todo: 重写
@ -200,10 +205,10 @@ function resize() {
if (!main) return;
if (has(props.width)) width.value = props.width;
if (has(props.height)) height.value = props.height;
if (has(props.left)) left.value = props.left;
if (has(props.top)) top.value = props.top;
if (!isNil(props.width)) width.value = props.width;
if (!isNil(props.height)) height.value = props.height;
if (!isNil(props.left)) left.value = props.left;
if (!isNil(props.top)) top.value = props.top;
const beforeWidth = width.value;
const beforeHeight = height.value;

View File

@ -9,7 +9,8 @@
<script lang="tsx" setup>
import { onMounted, onUnmounted, onUpdated } from 'vue';
import { addAnimate, removeAnimate } from '../animateController';
import { has, requireUniqueSymbol } from '../utils';
import { requireUniqueSymbol } from '../use';
import { isNil } from 'lodash-es';
const id = requireUniqueSymbol().toFixed(0);
@ -27,7 +28,7 @@ let ctx: CanvasRenderingContext2D;
let drawFn: () => void;
function draw() {
if (has(drawFn)) removeAnimate(drawFn);
if (!isNil(drawFn)) removeAnimate(drawFn);
const cls = core.getClsFromId(props.id as AllIds);
const frames = core.getAnimateFrames(cls);

View File

@ -26,8 +26,8 @@
import { onMounted, onUpdated } from 'vue';
import { LeftOutlined } from '@ant-design/icons-vue';
import Scroll from './scroll.vue';
import { isMobile } from '../use';
import { has, requireUniqueSymbol } from '../utils';
import { isMobile, requireUniqueSymbol } from '../use';
import { isNil } from 'lodash-es';
const emits = defineEmits<{
(e: 'close'): void;
@ -51,10 +51,10 @@ function resize() {
left = document.getElementById(`column-left-${id}`) as HTMLDivElement;
right = document.getElementById(`column-right-${id}`) as HTMLDivElement;
if (has(props.width) && !isMobile) main.style.width = `${props.width}%`;
if (has(props.height)) main.style.height = `${props.height}%`;
if (has(props.left)) left.style.flexBasis = `${props.left}%`;
if (has(props.right)) right.style.flexBasis = `${props.right}%`;
if (!isNil(props.width) && !isMobile) main.style.width = `${props.width}%`;
if (!isNil(props.height)) main.style.height = `${props.height}%`;
if (!isNil(props.left)) left.style.flexBasis = `${props.left}%`;
if (!isNil(props.right)) right.style.flexBasis = `${props.right}%`;
}
onMounted(async () => {

View File

@ -16,7 +16,7 @@
></BoxAnimate>
<div
class="special-text"
v-if="has(enemy.special) && enemy.special.length > 0"
v-if="!isNil(enemy.special) && enemy.special.length > 0"
>
<template v-for="(text, i) in enemy.showSpecial">
<span v-if="i < (isMobile ? 1 : 2)"
@ -110,10 +110,10 @@
</template>
<script lang="ts" setup>
import { has } from '../utils';
import BoxAnimate from '../components/boxAnimate.vue';
import { isMobile } from '../use';
import { ToShowEnemy } from '../tools/book';
import { isNil } from 'lodash-es';
const props = defineProps<{
enemy: ToShowEnemy;

View File

@ -4,11 +4,10 @@
<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue';
import { requireUniqueSymbol } from '../utils';
import { MinimapDrawer, getArea } from '../tools/fly';
import { useDrag, useWheel } from '../use';
import { useDrag, useWheel, requireUniqueSymbol } from '../use';
import { debounce } from 'lodash-es';
import { mainSetting } from '../preset/ui';
import { mainSetting } from '../preset/settingIns';
const props = defineProps<{
action?: boolean;

View File

@ -12,8 +12,12 @@
<script lang="ts" setup>
import { sleep } from 'mutate-animate';
import { onMounted, onUnmounted, onUpdated } from 'vue';
import { cancelGlobalDrag, useDrag, useWheel } from '../use';
import { requireUniqueSymbol } from '../utils';
import {
cancelGlobalDrag,
useDrag,
useWheel,
requireUniqueSymbol
} from '../use';
let main: HTMLDivElement;

View File

@ -1,5 +1,6 @@
import { Component, shallowReactive } from 'vue';
import { EventEmitter } from '@motajs/legacy-common';
import { IGameUi, IUiController } from './interface';
interface FocusEvent<T> {
focus: (before: T | null, after: T) => void;
@ -128,7 +129,7 @@ interface MountedVBind {
[x: string]: any;
}
export class GameUi extends EventEmitter<GameUiEvent> {
export class GameUi extends EventEmitter<GameUiEvent> implements IGameUi {
static uiList: GameUi[] = [];
component: Component;
@ -161,7 +162,10 @@ interface HoldOnController {
end(noClosePanel?: boolean): void;
}
export class UiController extends Focus<IndexedGameUi> {
export class UiController
extends Focus<IndexedGameUi>
implements IUiController
{
static list: UiController[] = [];
list: Record<string, GameUi> = {};
num: number = 0;

View File

@ -1,9 +1,11 @@
import { EventEmitter } from 'eventemitter3';
import { logger } from '@motajs/common';
import { deleteWith, ensureArray, parseCss, tip } from '@motajs/legacy-ui';
import { ResponseBase } from '@motajs/client-base';
import axios, { AxiosResponse, toFormData } from 'axios';
import { VNode, h, shallowReactive } from 'vue';
import { ensureArray, parseCss } from './utils';
import { deleteWith } from '@motajs/legacy-common';
import { tip } from './use';
// /* @__PURE__ */ import { id, password } from '../../../../user';
type CSSObj = Partial<Record<CanParseCss, string>>;

View File

@ -1,6 +1,7 @@
import { ensureArray, tip } from '@motajs/legacy-ui';
import { ensureArray } from '../utils';
import { sleep } from 'mutate-animate';
import { logger } from '@motajs/common';
import { tip } from '../use';
const { gl, gl2 } = checkSupport();
@ -54,10 +55,10 @@ type UniformFunc<
type UniformBinderValue<N extends UniformBinderNum> = N extends 1
? number
: N extends 2
? [number, number]
: N extends 3
? [number, number, number]
: [number, number, number, number];
? [number, number]
: N extends 3
? [number, number, number]
: [number, number, number, number];
interface UniformBinder<
N extends UniformBinderNum,

View File

@ -9,7 +9,8 @@ export * from './fx';
export * from './animateController';
export * from './controller';
export * from './danmaku';
export * from './mark';
// export * from './mark';
export * from './setting';
export * from './use';
export * from './utils';
export * from './uiUtils';

View File

@ -0,0 +1,99 @@
export interface IGameUi {
id: string;
symbol: symbol;
}
export interface IMountedVBind {
num: number;
ui: IGameUi;
controller: IUiController;
[x: string]: any;
}
interface HoldOnController {
end(noClosePanel?: boolean): void;
}
type UiVOn = Record<string, (param?: any) => void>;
type UiVBind = Record<string, any>;
export interface IUiController {
stack: any[];
/**
* 设置为仅显示最后一个ui
*/
showEnd(): void;
/**
* 设置为显示所有ui
*/
showAll(): void;
/**
* 根据id获取到ui
* @param id ui的id
*/
get(id: string): void;
/**
* 暂时保持下一次删除ui不会导致ui整体被关闭,引起ui背景闪烁。
* 例如可以用于道具栏,打开道具时就应当 holdOn,然后通过道具使用钩子来判断接下来是否要隐藏 app:
* ```txt
* hold on -> close -> use item -> hook -> stack.length === 0 ? end(): no action
* ```
*/
holdOn(): HoldOnController;
/**
* 关闭一个ui,注意如果不是平等模式,在其之后的ui都会同时关闭掉
* @param num 要关闭的ui的唯一标识符
*/
close(num: number): void;
/**
* 根据id关闭所有同id的ui,注意非平等模式下,会将第一个ui后的所有ui都关闭掉
* @param id 要关闭的ui的id
*/
closeByName(id: string): void;
/**
* 打开一个新的ui
* @param id 要打开的ui的id
* @param vOn 监听的事件
* @param vBind 绑定的数据
* @returns ui的唯一标识符
*/
open(id: string, vBind?: UiVBind, vOn?: UiVOn): void;
/**
* 注册一个ui
* @param id ui的id
* @param ui 对应的GameUi实例
*/
register(...ui: IGameUi[]): void;
/**
* 取消注册一个ui
* @param id 要取消注册的ui的id
*/
unregister(...id: string[]): void;
/**
* 根据ui的唯一标识符进行聚焦
* @param num 要聚焦于的ui的唯一标识符
*/
focusByNum(num: number): void;
/**
* 根据唯一标识符获取对应的ui
* @param num ui的唯一标识符
*/
getByNum(num: number): void;
/**
* 根据ui的唯一标识符来判断当前是否存在某个ui
* @param id ui的唯一标识符
*/
hasName(id: string): void;
}

View File

@ -1,164 +0,0 @@
import { IDamageEnemy } from '@motajs/types';
import { fixedUi } from './preset/ui';
import { tip } from './utils';
import { ref, Ref } from 'vue';
export interface MarkInfo<T extends EnemyIds> {
id: T;
enemy: IDamageEnemy;
/**
* 提示模式,从低到高位数分别为:
* 1. 踩临界时
* 2. 能打过怪物时
* 3. 小于勇士生命值的2/3时
* 4. 小于勇士生命值的1/3时
* 5. 零伤时
* 6. 小于指定伤害时
*/
mode: number;
/** 当前提示状态,提示模式的 2-6 */
status: number;
lastAtk: number;
lastDamage: number;
markDamage?: number;
/** 数据更新用,取反更新标记信息 */
update: Ref<boolean>;
}
const uiMap = new Map<EnemyIds, number>();
const marked: MarkInfo<EnemyIds>[] = [];
/**
* 标记一个怪物,标记后的怪物会在勇士刚好能打过怪物时、伤害刚好小于勇士生命值的2/3和1/3时、踩到临界时提示
* @param id 标记的怪物id
*/
export function markEnemy(id: EnemyIds) {
if (hasMarkedEnemy(id)) return;
const { DamageEnemy, getHeroStatusOn } = Mota.require('@user/data-state');
const enemy = new DamageEnemy(core.material.enemys[id]);
enemy.calAttribute();
enemy.getRealInfo();
const info: MarkInfo<EnemyIds> = {
id,
enemy,
mode: 0b011111,
lastAtk: getHeroStatusOn('atk', 'empty'),
lastDamage: enemy.calDamage().damage,
status: 0b0,
update: ref(true)
};
marked.push(info);
uiMap.set(id, fixedUi.open('markedEnemy', { enemy: info }));
tip('success', `已标记 ${enemy.enemy.name}!`);
}
export function unmarkEnemy(id: EnemyIds) {
fixedUi.close(uiMap.get(id) ?? -1);
uiMap.delete(id);
const index = marked.findIndex(v => v.id === id);
if (index === -1) return;
tip('success', `已取消标记 ${marked[index].enemy.enemy.name}!`);
marked.splice(index, 1);
}
export function checkMarkedEnemy() {
const { getHeroStatusOn } = Mota.require('@user/data-state');
marked.forEach(v => {
const { id, enemy, mode, lastAtk, lastDamage, markDamage } = v;
const atk = getHeroStatusOn('atk', 'empty');
let tip = 0;
if (mode & 0b11110) {
const damage = enemy.calDamage().damage;
const hp = core.status.hero.hp;
v.lastDamage = damage;
if (damage > lastDamage) return;
// 重置标记状态
if (damage > hp) {
v.status &= 0b100001;
}
if (damage > (markDamage ?? Infinity)) {
v.status &= 0b1;
}
// 能打过怪物提示、2/3提示、1/3提示、零伤提示、指定伤害提示
if (mode & (1 << 1) && damage < hp && damage > (hp * 2) / 3) {
if (!(v.status & (1 << 1))) {
v.status &= 0b100001;
v.status |= 1 << 1;
tip |= 1 << 1;
}
} else if (mode & (1 << 2) && damage > hp / 3) {
if (!(v.status & (1 << 2))) {
v.status &= 0b100011;
v.status |= 1 << 2;
tip |= 1 << 2;
}
} else if (mode & (1 << 3) && damage > 0) {
if (!(v.status & (1 << 3))) {
v.status &= 0b100111;
v.status |= 1 << 3;
tip |= 1 << 3;
}
} else if (mode & (1 << 4)) {
if (!(v.status & (1 << 4))) {
v.status &= 0b101111;
v.status |= 1 << 4;
tip |= 1 << 4;
}
}
if (mode & (1 << 5) && damage < (markDamage ?? Infinity)) {
if (!(v.status & (1 << 5))) {
if (damage < (markDamage ?? Infinity)) {
v.status |= 1 << 5;
} else {
v.status &= 0b011111;
}
}
}
}
// 临界提示
if (mode & (1 << 0)) {
const critical = enemy.calCritical(1)[0]?.atkDelta ?? Infinity;
v.lastAtk = atk + critical;
if (critical + atk > lastAtk) {
tip |= 1 << 0;
}
}
makeTip(id, tip, v);
v.update.value = !v.update.value;
});
}
function makeTip(enemy: EnemyIds, mode: number, info: MarkInfo<EnemyIds>) {
const name = core.material.enemys[enemy].name;
if (mode & (1 << 0)) {
tip('success', `已踩到 ${name} 的临界!`);
}
if (mode & (1 << 1)) {
tip('success', `已能打过 ${name}!`);
}
if (mode & (1 << 2)) {
tip('success', `${name} 的伤害已降至 2/3!`);
}
if (mode & (1 << 3)) {
tip('success', `${name} 的伤害已降至 1/3!`);
}
if (mode & (1 << 4)) {
tip('success', `${name} 已零伤!`);
}
if (mode & (1 << 5)) {
const damage = core.formatBigNumber(info.markDamage ?? Infinity);
tip('success', `${name} 的伤害已降至 ${damage}!`);
}
}
export function hasMarkedEnemy(id: EnemyIds) {
return marked.some(v => v.id === id);
}
const { hook } = Mota.require('@user/data-base');
hook.on('statusBarUpdate', () => {
checkMarkedEnemy();
});

View File

@ -67,12 +67,12 @@
>
<span class="changable" :change="nowDamageChangable"
><span style="font-family: 'FiraCode'">{{
(nowDamage[0] as number) < 0 && !has(enemy.damage)
(nowDamage[0] as number) < 0 && isNil(enemy.damage)
? '=>'
: ''
}}</span
>{{
(nowDamage[0] as number) < 0 && !has(enemy.damage)
(nowDamage[0] as number) < 0 && isNil(enemy.damage)
? format(-nowDamage[0])
: format(nowDamage[0])
}}</span
@ -96,8 +96,8 @@
import { computed, onMounted, ref, watch } from 'vue';
import { detailInfo, getCriticalDamage, getDefDamage } from '../tools/book';
import Chart, { ChartConfiguration } from 'chart.js/auto';
import { has, setCanvasSize } from '../utils';
import { debounce } from 'lodash-es';
import { setCanvasSize } from '../utils';
import { debounce, isNil } from 'lodash-es';
import { isMobile } from '../use';
import { createChangable } from '../tools/common';
@ -113,10 +113,10 @@ const ceil = Math.ceil;
const x = ref<number>();
const y = ref<number>();
x.value = has(x.value)
x.value = !isNil(x.value)
? Math.round(x.value + core.bigmap.offsetX / 32)
: void 0;
y.value = has(y.value)
y.value = !isNil(y.value)
? Math.round(y.value + core.bigmap.offsetY / 32)
: void 0;

View File

@ -1,85 +0,0 @@
<template>
<div id="enemy-target">
<div id="enemy-desc">
<span>怪物描述</span>
<Scroll id="enemy-desc-scroll">
<span
>&nbsp;&nbsp;&nbsp;&nbsp;{{
enemy.enemy.enemy.description
}}</span
>
</Scroll>
</div>
<a-divider dashed style="border-color: #ddd4"></a-divider>
<div>
<div id="mark-target">
<span
id="mark-info"
:style="{ color: marked ? 'lightgreen' : 'lightcoral' }"
>{{ marked ? '已标记该怪物' : '未标记该怪物' }}</span
>
<span class="button-text" @click.stop="mark">{{
marked ? '取消标记该怪物' : '标记该怪物为目标'
}}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import Scroll from '../components/scroll.vue';
import { detailInfo } from '../tools/book';
import { hasMarkedEnemy, markEnemy } from '../mark';
const enemy = detailInfo.enemy!;
const marked = ref(hasMarkedEnemy(enemy.enemy.id));
function mark() {
markEnemy(enemy.enemy.id);
marked.value = hasMarkedEnemy(enemy.enemy.id);
}
</script>
<style lang="less" scoped>
#enemy-target {
width: 100%;
font-size: 160%;
}
#enemy-desc {
width: 100%;
height: 30vh;
display: flex;
flex-direction: column;
align-items: center;
}
#enemy-desc-scroll {
height: 100%;
width: 100%;
}
#mark-target {
margin-top: 10%;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-around;
font-size: 3.3vh;
}
#mark-info {
transition: color 0.2s linear;
}
@media screen and (max-width: 600px) {
#enemy-desc {
font-size: 70%;
}
#mark-target {
font-size: 100%;
}
}
</style>

View File

@ -1,6 +1,6 @@
import { Danmaku } from '../danmaku';
import { Component, h } from 'vue';
import { mainSetting } from './ui';
import { mainSetting } from './settingIns';
import { getIconHeight } from '../utils';
import { BoxAnimate } from '../components';

View File

@ -1,5 +1,4 @@
import { debounce } from 'lodash-es';
import { fixedUi, mainUi } from './ui';
import { ref } from 'vue';
import { sleep } from 'mutate-animate';
@ -16,6 +15,7 @@ const showFixed = debounce((block: Block) => {
if (!e) return;
const enemy = core.status.thisMap.enemy.get(block.x, block.y);
if (!enemy) return;
const { fixedUi } = Mota.require('@motajs/legacy-ui');
fixedUi.open(
'fixed',
{ enemy, close, loc: [cx, cy], hovered },
@ -29,6 +29,7 @@ const showFixed = debounce((block: Block) => {
const closeFixed = () => {
close.value = true;
sleep(200).then(() => {
const { fixedUi } = Mota.require('@motajs/legacy-ui');
fixedUi.closeByName('fixed');
close.value = false;
});
@ -58,6 +59,7 @@ gameListener.on('mouseMove', e => {
});
hook.once('mounted', () => {
const { mainUi } = Mota.require('@motajs/legacy-ui');
mainUi.on('start', () => {
showFixed.cancel();
closeFixed();

View File

@ -1,15 +1,11 @@
import { KeyCode } from '@motajs/client-base';
import { gameKey, HotkeyJSON } from '@motajs/system-action';
import {
openDanmakuPoster,
tip,
hasMarkedEnemy,
markEnemy,
unmarkEnemy
} from '@motajs/legacy-ui';
import { hovered } from './fixed';
import { mainUi } from './ui';
import { mainUi } from './uiIns';
import { GameStorage } from '@motajs/legacy-system';
// import { hasMarkedEnemy, markEnemy, unmarkEnemy } from '../mark';
import { openDanmakuPoster } from '../uiUtils';
import { tip } from '../use';
export const mainScope = Symbol.for('@key_main');
@ -506,9 +502,9 @@ gameKey
.realize('mark', () => {
const cls = hovered?.event.cls;
if (cls === 'enemys' || cls === 'enemy48') {
const id = hovered!.event.id as EnemyIds;
if (hasMarkedEnemy(id)) unmarkEnemy(id);
else markEnemy(id);
// const id = hovered!.event.id as EnemyIds;
// if (hasMarkedEnemy(id)) unmarkEnemy(id);
// else markEnemy(id);
}
})
.realize('special', () => {

View File

@ -4,3 +4,5 @@ export * from './danmaku';
export * from './fixed';
export * from './hotkey';
export * from './keyboard';
export * from './uiIns';
export * from './settingIns';

View File

@ -0,0 +1,3 @@
import { MotaSetting } from '../setting';
export const mainSetting = new MotaSetting();

View File

@ -1,6 +1,6 @@
import type { SettingComponent, SettingComponentProps } from '../setting';
import { Button, InputNumber, Radio } from 'ant-design-vue';
import { mainUi } from './ui';
import { mainUi } from './uiIns';
import { gameKey } from '@motajs/system-action';
interface Components {

View File

@ -1,50 +1,13 @@
import { GameStorage, VirtualKey } from '@motajs/legacy-system';
import {
createSettingComponents,
GameUi,
isMobile,
MotaSetting,
triggerFullscreen,
UI,
UiController
} from '@motajs/legacy-ui';
import { GameStorage } from '@motajs/legacy-system';
import { createSettingComponents } from './settings';
import { isMobile } from '../use';
import { MotaSetting } from '../setting';
import { triggerFullscreen } from '../utils';
import settingsText from '../data/settings.json';
import { fixedUi, mainUi } from './uiIns';
import { mainSetting } from './settingIns';
//#region legacy-ui
export const mainUi = new UiController();
mainUi.register(
new GameUi('book', UI.Book),
new GameUi('toolbox', UI.Toolbox),
new GameUi('equipbox', UI.Equipbox),
new GameUi('settings', UI.Settings),
new GameUi('desc', UI.Desc),
new GameUi('skill', UI.Skill),
new GameUi('skillTree', UI.SkillTree),
new GameUi('fly', UI.Fly),
new GameUi('fixedDetail', UI.FixedDetail),
new GameUi('shop', UI.Shop),
new GameUi('achievement', UI.Achievement),
new GameUi('hotkey', UI.Hotkey),
new GameUi('toolEditor', UI.ToolEditor),
new GameUi('virtualKey', VirtualKey)
// todo: 把游戏主 div 加入到 mainUi 里面
);
mainUi.showAll();
export const fixedUi = new UiController(true);
fixedUi.register(
new GameUi('markedEnemy', UI.Marked),
new GameUi('fixed', UI.Fixed),
new GameUi('chapter', UI.Chapter),
new GameUi('completeAchi', UI.CompleteAchi),
new GameUi('start', UI.Start),
new GameUi('toolbar', UI.Toolbar),
new GameUi('load', UI.Load),
new GameUi('danmaku', UI.Danmaku),
new GameUi('danmakuEditor', UI.DanmakuEditor),
new GameUi('tips', UI.Tips)
);
fixedUi.showAll();
const { hook } = Mota.require('@user/data-base');
hook.once('mounted', () => {
@ -84,7 +47,6 @@ hook.once('mounted', () => {
const COM = createSettingComponents();
export const mainSetting = new MotaSetting();
// 添加不参与全局存储的设置
MotaSetting.noStorage.push('action.autoSkill', 'screen.fullscreen');

View File

@ -0,0 +1,38 @@
import { GameUi, UiController } from '../controller';
import * as UI from '../ui';
import { VirtualKey } from '@motajs/legacy-system';
export const mainUi = new UiController();
mainUi.register(
new GameUi('book', UI.Book),
new GameUi('toolbox', UI.Toolbox),
new GameUi('equipbox', UI.Equipbox),
new GameUi('settings', UI.Settings),
new GameUi('desc', UI.Desc),
new GameUi('skill', UI.Skill),
new GameUi('skillTree', UI.SkillTree),
new GameUi('fly', UI.Fly),
new GameUi('fixedDetail', UI.FixedDetail),
new GameUi('shop', UI.Shop),
// new GameUi('achievement', UI.Achievement),
new GameUi('hotkey', UI.Hotkey),
new GameUi('toolEditor', UI.ToolEditor),
new GameUi('virtualKey', VirtualKey)
// todo: 把游戏主 div 加入到 mainUi 里面
);
mainUi.showAll();
export const fixedUi = new UiController(true);
fixedUi.register(
new GameUi('markedEnemy', UI.Marked),
new GameUi('fixed', UI.Fixed),
new GameUi('chapter', UI.Chapter),
new GameUi('completeAchi', UI.CompleteAchi),
new GameUi('start', UI.Start),
new GameUi('toolbar', UI.Toolbar),
new GameUi('load', UI.Load),
new GameUi('danmaku', UI.Danmaku),
new GameUi('danmakuEditor', UI.DanmakuEditor),
new GameUi('tips', UI.Tips)
);
fixedUi.showAll();

View File

@ -1,9 +1,6 @@
import { FunctionalComponent, reactive } from 'vue';
import { EventEmitter } from '@motajs/legacy-common';
import { has } from './utils';
import { createSettingComponents } from './preset';
const COM = createSettingComponents();
import { isNil } from 'lodash-es';
export interface SettingComponentProps {
item: MotaSettingItem;
@ -85,7 +82,7 @@ export class MotaSetting extends EventEmitter<SettingEvent> {
key: string,
name: string,
value: MotaSettingType,
com: SettingComponent = COM.Default,
com: SettingComponent,
step: [number, number, number] = [0, 100, 1]
) {
const setting: MotaSettingItem = {
@ -162,12 +159,12 @@ export class MotaSetting extends EventEmitter<SettingEvent> {
defaultValue?: T
): T | undefined {
const setting = this.getSetting(key);
if (!has(setting) && !has(defaultValue)) return void 0;
if (isNil(setting) && isNil(defaultValue)) return void 0;
if (setting instanceof MotaSetting) {
if (has(setting)) return defaultValue;
if (!isNil(setting)) return defaultValue;
return void 0;
} else {
return has(setting) ? (setting.value as T) : (defaultValue as T);
return !isNil(setting) ? (setting.value as T) : (defaultValue as T);
}
}

View File

@ -1,120 +0,0 @@
import list from '../data/achievement.json';
import { achiDict, checkCompletionAchievement } from './completion';
import { changeLocalStorage, has } from '../utils';
import { fixedUi } from '../preset';
type AchievementList = typeof list;
export type AchievementType = keyof AchievementList;
type AchievementData = Record<AchievementType, boolean[]>;
export interface Achievement {
name: string;
text: string[];
point: number;
hide?: string;
progress?: string;
percent?: boolean;
}
export default function init() {
return { completeAchievement, hasCompletedAchievement, addMountSign };
}
export const totalPoint = Object.values(list)
.map((v: Achievement[]) =>
v.reduce((prev, curr) => {
return curr.point + prev;
}, 0)
)
.reduce((prev, curr) => prev + curr);
/**
* 完成一个成就
* @param type 成就类型
* @param index 成就索引
*/
export function completeAchievement(type: AchievementType, index: number) {
if (flags.debug || hasCompletedAchievement(type, index)) return;
changeLocalStorage<AchievementData>(
'achievement',
data => {
data[type][index] = true;
return data;
},
{
normal: [],
challenge: [],
explore: []
}
);
if (type === 'explore' && !Object.values(achiDict).includes(index)) {
checkCompletionAchievement();
}
fixedUi.open('completeAchi', {
complete: `${type},${index}`
});
}
/**
* 是否完成了某个成就
* @param type 成就类型
* @param index 成就索引
*/
export function hasCompletedAchievement(type: AchievementType, index: number) {
let data = core.getLocalStorage<AchievementData>('achievement');
if (!has(data)) {
const d = {
normal: [],
challenge: [],
explore: []
};
data = d;
core.setLocalStorage('achievement', d);
}
return data[type][index] ?? false;
}
/**
* 获取当前成就点数
*/
export function getNowPoint() {
let res = 0;
for (const [type, achi] of Object.entries(list)) {
achi.forEach((v, i) => {
if (hasCompletedAchievement(type as AchievementType, i)) {
res += v.point;
}
});
}
return res;
}
// ----- 各个成就相关的函数
/**
* 山路木牌
* @param id 木牌id
*/
export function addMountSign(id: number) {
if (flags.debug) return;
if (
!core.getLocalStorage(`mountSign_${id}`, false) &&
!hasCompletedAchievement('explore', 1)
) {
changeLocalStorage(
'mountSign',
n => {
if (n + 1 >= 5) {
completeAchievement('explore', 1);
for (const i of [1, 2, 3, 4, 5]) {
core.removeLocalStorage(`mountSign_${i}`);
}
}
return n + 1;
},
0
);
core.setLocalStorage(`mountSign_${id}`, true);
}
}

View File

@ -1,4 +1,4 @@
import { has } from '../utils';
import { isNil } from 'lodash-es';
import { IDamageEnemy } from '@motajs/types';
export interface CurrentEnemy {
@ -73,7 +73,7 @@ export function getDefDamage(
if (res.length === 0) {
origin = dam.damage;
if (has(origin)) {
if (!isNil(origin)) {
res.push([addDef + i * ratio, origin]);
last = origin;
}
@ -113,7 +113,7 @@ export function getCriticalDamage(
if (res.length === 0) {
origin = dam.damage;
if (has(origin)) {
if (!isNil(origin)) {
res.push([addAtk + i * ratio, origin]);
last = origin;
}

View File

@ -1,113 +0,0 @@
import {
AchievementType,
completeAchievement,
hasCompletedAchievement
} from './achievement';
import { changeLocalStorage } from '../utils';
import list from '../data/achievement.json';
export const floors: Record<number, FloorIds[]> = {
1: ['MT0', 'tower7']
};
const achis: Record<number, Record<AchievementType, number[]>> = {
1: {
normal: [0, 1],
challenge: [0],
explore: [1]
}
};
export const achiDict: Record<number, number> = {
1: 0
};
export function init() {
Object.values(floors).forEach((v, i) => {
const from = core.floorIds.indexOf(v[0]);
const to = core.floorIds.indexOf(v[1]);
const all = core.floorIds.slice(from, to + 1);
floors[i + 1] = all;
});
}
/**
* 检查所有到达过的楼层,用于成就的计算
*/
export function checkVisitedFloor() {
changeLocalStorage<Partial<Record<FloorIds, boolean>>>(
'visitedFloor',
data => {
let needUpdate = false;
core.floorIds.forEach(v => {
if (core.hasVisitedFloor(v)) {
data[v] = true;
needUpdate = true;
}
});
if (needUpdate) {
checkCompletionAchievement();
}
return data;
},
{}
);
}
/**
* 获取一个章节的完成度
* @param num 章节
*/
export function getChapterCompletion(num: number) {
if (!achis[num]) return 0;
let res = 0;
const all = floors[num];
const achiNum = Object.values(achis[num]).reduce(
(pre, cur) => pre + cur.length,
0
);
// 计算到达过的楼层
let visitedFloor = 0;
const visited = core.getLocalStorage<Partial<Record<FloorIds, boolean>>>(
'visitedFloor',
{}
);
all.forEach(v => {
if (visited[v]) visitedFloor++;
});
const floorRatio = all.length / (all.length + achiNum);
const floorPoint = (floorRatio * visitedFloor) / all.length;
let completedPoint = 0;
let totalPoint = 0;
// 计算成就,占比按成就点走
for (const [type, achi] of Object.entries(achis[num]) as [
AchievementType,
number[]
][]) {
achi.forEach(v => {
totalPoint += list[type][v].point;
if (hasCompletedAchievement(type, v)) {
completedPoint += list[type][v].point;
}
});
}
const achiPoint = (completedPoint / totalPoint) * (1 - floorRatio);
res = floorPoint + achiPoint;
return Math.floor(res * 100);
}
/**
* 检查完成度成就是否完成
*/
export function checkCompletionAchievement() {
[1].forEach(v => {
if (getChapterCompletion(v) >= 100) {
completeAchievement('explore', achiDict[v]);
}
});
}

View File

@ -1,4 +1,5 @@
import { getStatusLabel, has } from '../utils';
import { isNil } from 'lodash-es';
import { getStatusLabel } from '../utils';
/**
* 获取所有装备
@ -71,7 +72,7 @@ export function getNowStatus(nowEquip?: Equip, onCol: boolean = false) {
else status = getHeroStatusOn(v)?.toString();
let add = 0;
if (has(nowEquip)) {
if (!isNil(nowEquip)) {
add += Math.floor(
(nowEquip.value[v] ?? 0) * core.getBuff(v)
);

View File

@ -1,5 +1,7 @@
import { mainSetting } from '../preset/ui';
import { downloadCanvasImage, has, tip } from '../utils';
import { isNil } from 'lodash-es';
import { mainSetting } from '../preset/settingIns';
import { tip } from '../use';
import { downloadCanvasImage } from '../utils';
type BFSFromString = `${FloorIds},${number},${number},${Dir}`;
type BFSToString = `${FloorIds},${number},${number}`;
@ -163,7 +165,7 @@ export function getMapData(
noCache: boolean = false
): MapBFSResult {
if (!floorId) return { maps: [], link: {} };
if (has(bfsCache[floorId]) && !noCache) return bfsCache[floorId]!;
if (!isNil(bfsCache[floorId]) && !noCache) return bfsCache[floorId]!;
const queue = [floorId];
const used: Partial<Record<FloorIds, boolean>> = {

View File

@ -1,7 +1,10 @@
export * from './achievement';
// import { init } from './achievement';
// init();
// export * from './achievement';
export * from './book';
export * from './common';
export * from './completion';
export * from './equipbox';
export * from './fixed';
export * from './fly';

View File

@ -1,340 +0,0 @@
<template>
<div id="achievement">
<div id="tools">
<span id="back" class="button-text tools" @click="exit"
><left-outlined />返回游戏</span
>
</div>
<div id="column">
<div class="achievement-column" v-for="c of column">
<span
class="column-text button-text"
:active="selectedColumn === c"
@click="selectedColumn = c"
>{{ columnName[c] }}</span
>
</div>
</div>
<a-divider dashed id="divider"></a-divider>
<div id="list">
<div id="achievement-list" :style="{ left: `-${offset}%` }">
<div v-for="t of column" class="achievement-one">
<Scroll class="list-scroll" :width="isMobile ? 10 : 20">
<div class="list-div">
<div
v-for="a of getAllAchievements(t)"
class="list-one"
>
<div
class="list-content"
:complete="a.complete"
>
<span class="list-name">{{ a.name }}</span>
<span
class="list-text"
v-html="a.text"
></span>
<div class="list-end">
<div class="end-info">
<span
class="complete"
:complete="a.complete"
>完成情况:
{{
a.complete
? '已完成'
: '未完成'
}}</span
>
<span class="point"
>成就点数: {{ a.point }}</span
>
</div>
<div
v-if="a.progress"
class="list-progress"
>
<a-progress
:percent="a.percent"
:strokeColor="{
'0%': '#108ee9',
'100%': '#87d068'
}"
:strokeWidth="height / 150"
:format="
() =>
a.usePercent
? `${a.percent}%`
: a.progress
"
></a-progress>
</div>
</div>
</div>
<a-divider id="divider" dashed></a-divider>
</div>
</div>
</Scroll>
</div>
</div>
</div>
<div id="total-progress">
<a-progress
id="point-progress"
:percent="(nowPoint / totalPoint) * 100"
:strokeColor="{
'0%': '#108ee9',
'100%': '#87d068'
}"
:strokeWidth="height / 150"
:showInfo="false"
></a-progress>
<span id="point-number"
>成就点: {{ nowPoint }} / {{ totalPoint }}</span
>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { LeftOutlined } from '@ant-design/icons-vue';
import list from '../data/achievement.json';
import {
Achievement,
getNowPoint,
hasCompletedAchievement
} from '../tools/achievement';
import Scroll from '../components/scroll.vue';
import { isMobile } from '../use';
import { mainUi } from '../preset';
const props = defineProps<{
num: number;
}>();
type AchievementList = typeof list;
type AchievementType = keyof AchievementList;
interface ResolvedAchievement {
name: string;
text: string;
complete: boolean;
point: number;
/** number / number */
progress?: string;
percent?: number;
usePercent?: boolean;
}
const column: AchievementType[] = ['normal', 'challenge', 'explore'];
const columnName = {
normal: '普通成就',
challenge: '挑战成就',
explore: '探索成就'
};
const selectedColumn = ref<AchievementType>('normal');
const offset = computed(() => {
return column.indexOf(selectedColumn.value) * 100;
});
const height = window.innerHeight;
const width = window.innerWidth;
const totalPoint = Object.values(list)
.map((v: Achievement[]) =>
v.reduce((prev, curr) => {
return curr.point + prev;
}, 0)
)
.reduce((prev, curr) => prev + curr);
const nowPoint = getNowPoint();
/**
* 获取一个类型的所有成就
* @param type 成就类型
*/
function getAllAchievements(type: AchievementType): ResolvedAchievement[] {
return list[type].map<ResolvedAchievement>((v: Achievement, i) => {
const complete = hasCompletedAchievement(type, i);
const text = v.hide && !complete ? v.hide : v.text.join('');
const res: ResolvedAchievement = {
text,
name: v.name,
point: v.point,
complete
};
if (v.progress) {
const p = eval('`' + v.progress + '`') as string;
res.progress = p;
res.percent = Math.floor(eval(p) * 100);
if (v.percent) res.usePercent = true;
}
return res;
});
}
function exit() {
mainUi.close(props.num);
}
</script>
<style lang="less" scoped>
#achievement {
width: 90vh;
height: 90vh;
font-family: 'normal';
font-size: 150%;
display: flex;
flex-direction: column;
user-select: none;
}
#divider {
margin: 1% 0;
border-color: #ddd4;
}
#tools {
height: 5vh;
font-size: 3.2vh;
}
#column {
display: flex;
flex-direction: row;
justify-content: space-around;
margin-top: 3%;
font-size: 130%;
}
.list-scroll {
width: 100%;
height: 100%;
}
#list {
overflow: hidden;
width: 100%;
height: max-content;
}
#achievement-list {
position: relative;
width: 300%;
height: 100%;
display: flex;
flex-direction: row;
transition: left 0.4s ease;
}
.achievement-one {
width: 90vh;
}
.list-div {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.list-one {
width: 70%;
.list-content {
height: 18vh;
display: flex;
flex-direction: column;
align-items: center;
border: 2px double rgba(132, 132, 132, 0.17);
border-radius: 10px;
margin: 2% 0 2.5% 0;
background-color: rgba(59, 59, 59, 0.281);
}
.list-content[complete='true'] {
background-color: rgba(239, 255, 63, 0.205);
}
.list-name {
border-bottom: 1px solid #ddd4;
}
.list-text {
font-size: 100%;
padding: 0 20px;
}
.list-end {
width: 90%;
height: 95%;
display: flex;
flex-direction: column-reverse;
font-size: 90%;
.end-info {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: end;
}
.complete {
color: lightcoral;
}
.complete[complete='true'] {
color: lightgreen;
}
}
.list-progress {
display: flex;
flex-direction: row;
align-items: center;
.progress {
width: 100%;
}
}
}
#total-progress {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
#point-progress {
width: 100%;
}
#point-number {
font-size: 70%;
margin-left: 2%;
white-space: nowrap;
}
}
@media screen and (max-width: 600px) {
#achievement {
width: 90vw;
height: 90vh;
}
.list-one {
width: 90%;
.list-content {
height: 15vh;
}
.list-end {
margin-bottom: 0.8vh;
}
}
}
</style>

View File

@ -41,21 +41,17 @@
import { onUnmounted, ref } from 'vue';
import EnemyOne from '../components/enemyOne.vue';
import Scroll from '../components/scroll.vue';
import { has } from '../utils';
import BookDetail from './bookDetail.vue';
import { LeftOutlined } from '@ant-design/icons-vue';
import { ToShowEnemy, detailInfo } from '../tools/book';
import { getDetailedEnemy } from '../tools/fixed';
import { GameUi } from '../controller';
import { gameKey } from '@motajs/system-action';
import { mainUi } from '../preset/ui';
import { mainSetting } from '../preset/ui';
import { mainSetting } from '../preset/settingIns';
import { isMobile } from '../use';
import { IMountedVBind } from '../interface';
import { isNil } from 'lodash-es';
const props = defineProps<{
num: number;
ui: GameUi;
}>();
const props = defineProps<IMountedVBind>();
const floorId =
// @ts-ignore
@ -122,12 +118,12 @@ async function show() {
* 退出怪物手册
*/
async function exit() {
const hold = mainUi.holdOn();
mainUi.close(props.num);
const hold = props.controller.holdOn();
props.controller.close(props.num);
if (core.events.recoverEvents(core.status.event.interval)) {
hold.end(true);
return;
} else if (has(core.status.event.ui)) {
} else if (!isNil(core.status.event.ui)) {
core.status.boxAnimateObjs = [];
// @ts-ignore
core.ui._drawViewMaps(core.status.event.ui);

View File

@ -22,7 +22,7 @@
:from-book="fromBook"
v-else-if="panel === 'critical'"
></EnemyCritical>
<EnemyTarget v-else-if="panel === 'target'"></EnemyTarget>
<!-- <EnemyTarget v-else-if="panel === 'target'"></EnemyTarget> -->
</Transition>
<div id="detail-more">
<Transition name="detail">
@ -35,8 +35,8 @@
id="enemy-target"
class="button-text more"
@click="changePanel($event, 'target')"
><LeftOutlined /> 怪物更多信息</span
>
><LeftOutlined />
</span>
<span
id="critical-more"
class="button-text more"
@ -81,7 +81,7 @@ import { useDrag } from '../use';
import EnemySpecial from '../panel/enemySpecial.vue';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
import EnemyCritical from '../panel/enemyCritical.vue';
import EnemyTarget from '../panel/enemyTarget.vue';
// import EnemyTarget from '../panel/enemyTarget.vue';
import { detailInfo } from '../tools/book';
import { gameKey } from '@motajs/system-action';

View File

@ -1,22 +1,17 @@
<template>
<div id="chapter">
<canvas id="chapter-back"></canvas>
<span id="chapter-text">{{ chapter }}</span>
<span id="chapter-text">{{ props.chapter }}</span>
</div>
</template>
<script lang="ts" setup>
import { Animation, hyper, sleep } from 'mutate-animate';
import { onMounted } from 'vue';
import { has } from '../utils';
import { GameUi } from '../controller';
import { fixedUi } from '../preset/ui';
import { IMountedVBind } from '../interface';
import { isNil } from 'lodash-es';
const props = defineProps<{
num: number;
ui: GameUi;
chapter: string;
}>();
const props = defineProps<IMountedVBind>();
let can: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D;
@ -50,14 +45,14 @@ onMounted(async () => {
let started = false;
ani.ticker.add(time => {
if (!has(time) || isNaN(time)) return;
if (isNil(time) || isNaN(time)) return;
if (!started) {
started = true;
return;
}
if (time >= 4050) {
fixedUi.close(props.num);
props.controller.close(props.num);
ani.ticker.destroy();
}

View File

@ -1,113 +0,0 @@
<template>
<Box id="complete-box">
<div id="complete">
<span>完成成就&nbsp;&nbsp;&nbsp;&nbsp;{{ achi.name }}</span>
<a-progress
id="progress"
:percent="progress * 100"
:strokeColor="{
'0%': '#108ee9',
'100%': '#87d068'
}"
:strokeWidth="height / 200"
:showInfo="false"
></a-progress>
<span id="point-number">成就点: {{ now }} / {{ totalPoint }}</span>
</div>
</Box>
</template>
<script lang="ts" setup>
import { sleep, Ticker } from 'mutate-animate';
import { computed, onMounted, ref } from 'vue';
import Box from '../components/box.vue';
import list from '../data/achievement.json';
import { AchievementType, getNowPoint, totalPoint } from '../tools/achievement';
import { GameUi } from '../controller';
import { fixedUi } from '../preset/ui';
const height = window.innerHeight;
const props = defineProps<{
num: number;
ui: GameUi;
complete: string;
}>();
const c = props.complete.split(',');
const type = c[0] as AchievementType;
const index = parseInt(c[1]);
const achi = list[type][index];
const point = achi.point;
const nowPoint = getNowPoint() - point;
const now = ref(nowPoint);
const progress = computed(() => now.value / totalPoint);
onMounted(async () => {
await sleep(500);
const ticker = new Ticker();
const time = Date.now();
ticker.add(() => {
const nowTime = Date.now();
if (nowTime - time > 1000) {
now.value = nowPoint + point;
ticker.destroy();
}
const ratio = (nowTime - time) / 1000;
now.value = Math.floor(nowPoint + point * ratio);
});
await sleep(4600);
fixedUi.close(props.num);
});
</script>
<style lang="less" scoped>
#complete-box {
width: 30vw;
height: 13vh;
left: 35vw;
position: fixed;
background-color: #000d;
animation: ani 5s ease 0s 1 forwards running;
z-index: 10000;
}
#complete {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
font-family: 'normal';
font-size: 2.2vh;
align-items: center;
justify-content: center;
}
#progress {
width: 90%;
}
@keyframes ani {
0% {
top: -30vh;
}
20% {
top: 4vh;
}
80% {
top: 4vh;
}
100% {
top: -30vh;
}
}
@media screen and (max-width: 600px) {
#complete-box {
width: 90vw;
left: 5%;
}
}
</style>

View File

@ -30,7 +30,7 @@
import { nextTick, onUnmounted, reactive, watch } from 'vue';
import { Danmaku } from '../danmaku';
import { LikeFilled } from '@ant-design/icons-vue';
import { mainSetting } from '../preset/ui';
import { mainSetting } from '../preset/settingIns';
import { debounce } from 'lodash-es';
interface ElementMap {
@ -205,10 +205,7 @@ onUnmounted(() => {});
}
.danmaku-info {
text-shadow:
1px 1px 1px black,
1px -1px 1px black,
-1px 1px 1px black,
text-shadow: 1px 1px 1px black, 1px -1px 1px black, -1px 1px 1px black,
-1px -1px 1px black;
}

View File

@ -159,21 +159,17 @@ import {
UpOutlined
} from '@ant-design/icons-vue';
import { Danmaku } from '../danmaku';
import { GameUi } from '../controller';
import { sleep } from 'mutate-animate';
import { fixedUi } from '../preset/ui';
import { calStringSize, tip } from '../utils';
import { calStringSize, stringifyCSS, parseCss, getIconHeight } from '../utils';
import { gameKey } from '@motajs/system-action';
import { isNil } from 'lodash-es';
import { stringifyCSS, parseCss, getIconHeight } from '../utils';
import { logger, LogLevel } from '@motajs/common';
import Scroll from '../components/scroll.vue';
import BoxAnimate from '../components/boxAnimate.vue';
import { IMountedVBind } from '../interface';
import { tip } from '../use';
const props = defineProps<{
num: number;
ui: GameUi;
}>();
const props = defineProps<IMountedVBind>();
const frequentlyIcon: (AllIds | 'hero' | `X${number}`)[] = [
'hero',
@ -301,7 +297,7 @@ function close() {
mainDiv.classList.remove('danmaku-startup');
mainDiv.classList.add('danmaku-close');
sleep(200).then(() => {
fixedUi.close(props.num);
props.controller.close(props.num);
});
}

View File

@ -22,21 +22,17 @@ import { computed, onUnmounted, ref } from 'vue';
import desc from '../data/desc.json';
import { splitText } from '../utils';
import Colomn from '../components/colomn.vue';
import { GameUi } from '../controller';
import { gameKey } from '@motajs/system-action';
import { mainUi } from '../preset/ui';
import { IMountedVBind } from '../interface';
const props = defineProps<{
num: number;
ui: GameUi;
}>();
const props = defineProps<IMountedVBind>();
type DescKey = keyof typeof desc;
const selected = ref(Object.keys(desc)[0] as DescKey);
function exit() {
mainUi.close(props.num);
props.controller.close(props.num);
}
const content = computed(() => {

View File

@ -125,8 +125,8 @@
<BoxAnimate
:id="
isCol
? (equiped[selected] ?? 'none')
: (toShow[selected]?.[0] ?? 'none')
? equiped[selected] ?? 'none'
: toShow[selected]?.[0] ?? 'none'
"
></BoxAnimate>
<span>{{ equip.name }}</span>
@ -183,18 +183,14 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import Scroll from '../components/scroll.vue';
import { getAddStatus, getEquips, getNowStatus } from '../tools/equipbox';
import BoxAnimate from '../components/boxAnimate.vue';
import { has, tip, type } from '../utils';
import { cancelGlobalDrag, isMobile, useDrag } from '../use';
import { type, getStatusLabel } from '../utils';
import { cancelGlobalDrag, isMobile, tip, useDrag } from '../use';
import { hyper } from 'mutate-animate';
import { GameUi } from '../controller';
import { gameKey } from '@motajs/system-action';
import { getStatusLabel } from '../utils';
import { mainUi } from '../preset/ui';
import { IMountedVBind } from '../interface';
import { isNil } from 'lodash-es';
const props = defineProps<{
num: number;
ui: GameUi;
}>();
const props = defineProps<IMountedVBind>();
const equips = ref(getEquips());
const col = ref('all');
@ -242,10 +238,10 @@ const equip = computed(() => {
if (isCol.value) {
const id = equiped.value[selected.value];
const e = core.material.items[id];
if (!has(e)) return none;
if (isNil(e)) return none;
return e;
}
if (!has(index)) return none;
if (isNil(index)) return none;
return all[index[0]];
});
@ -276,7 +272,7 @@ const toShow = computed(() => {
const e = all[v[0]].equip!;
const t = e.type;
if (sortNorm !== 'none') {
if (!has(e[sortBy][sortNorm])) return false;
if (isNil(e[sortBy][sortNorm])) return false;
}
if (col.value === 'all') return true;
if (typeof t === 'string') return t === col.value;
@ -309,7 +305,7 @@ function changeSort() {
}
function exit() {
mainUi.close(props.num);
props.controller.close(props.num);
}
function clickList(i: number) {
@ -350,7 +346,7 @@ function canDragin(type: number) {
if (type < 0) return false;
const et = equip.value.equip?.type;
if (!core.canEquip(toShow.value[selected.value]?.[0])) return false;
if (!has(et)) return false;
if (isNil(et)) return false;
if (typeof et === 'number') return type === et;
return equipCol[type] === et;
}
@ -427,10 +423,10 @@ function dragout(e: Event) {
}
function toTool() {
mainUi.holdOn();
props.controller.holdOn();
exit();
nextTick(() => {
mainUi.open('toolbox');
props.controller.open('toolbox');
});
}

View File

@ -31,13 +31,11 @@
<script lang="ts" setup>
import { onMounted, onUpdated, Ref, ref, watch } from 'vue';
import Box from '../components/box.vue';
import { GameUi } from '../controller';
import { nextFrame } from '../utils';
import { EnemyInfo, IDamageEnemy } from '@motajs/types';
const props = defineProps<{
num: number;
ui: GameUi;
enemy: IDamageEnemy;
close: Ref<boolean>;
loc: [x: number, y: number];

View File

@ -13,14 +13,9 @@ import { getDetailedEnemy } from '../tools/fixed';
import BookDetail from './bookDetail.vue';
import { detailInfo } from '../tools/book';
import { hovered } from '../preset/fixed';
import { GameUi } from '../controller';
import { mainUi } from '../preset/ui';
import { IMountedVBind } from '../interface';
const props = defineProps<{
num: number;
ui: GameUi;
panel?: 'special' | 'critical' | 'target';
}>();
const props = defineProps<IMountedVBind>();
const panel = props.panel ?? 'special';
@ -40,7 +35,7 @@ if (hovered) {
}
function close() {
mainUi.close(props.num);
props.controller.close(props.num);
}
</script>

View File

@ -98,18 +98,14 @@ import {
DoubleRightOutlined
} from '@ant-design/icons-vue';
import { debounce } from 'lodash-es';
import { tip } from '../utils';
import { GameUi } from '../controller';
import { tip } from '../use';
import { gameKey } from '@motajs/system-action';
import { createChangable } from '../tools/common';
import { mainUi } from '../preset/ui';
import { mainSetting } from '../preset/ui';
import { mainSetting } from '../preset/settingIns';
import { GameStorage } from '@motajs/legacy-system';
import { IMountedVBind } from '../interface';
const props = defineProps<{
num: number;
ui: GameUi;
}>();
const props = defineProps<IMountedVBind>();
type Loc2 = [number, number, number, number];
@ -162,7 +158,7 @@ let thumb: HTMLCanvasElement;
let thumbCtx: CanvasRenderingContext2D;
function exit() {
mainUi.close(props.num);
props.controller.close(props.num);
}
const title = computed(() => {
@ -584,10 +580,7 @@ onUnmounted(() => {
max-width: 50%;
text-overflow: ellipsis;
overflow: hidden;
text-shadow:
1px 1px 1px black,
1px -1px 1px black,
-1px 1px 1px black,
text-shadow: 1px 1px 1px black, 1px -1px 1px black, -1px 1px 1px black,
-1px -1px 1px black;
}

View File

@ -37,14 +37,13 @@
<script lang="ts" setup>
import { Hotkey } from '@motajs/system-action';
import { GameUi } from '../controller';
import Column from '../components/colomn.vue';
import { mainUi } from '../preset/ui';
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue';
import { KeyCode, KeyCodeUtils } from '@motajs/client-base';
import { generateBinary, keycode } from '../utils';
import { cloneDeep } from 'lodash-es';
import { gameKey } from '@motajs/system-action';
import { generateBinary, keycode } from '@motajs/legacy-common';
import { IMountedVBind } from '../interface';
interface HotkeyKeys {
index: number;
@ -64,16 +63,12 @@ interface SelectedKey {
index: number;
}
const props = defineProps<{
num: number;
ui: GameUi;
hotkey: Hotkey;
}>();
const props = defineProps<IMountedVBind & { hotkey: Hotkey }>();
const hotkey = props.hotkey;
function close() {
mainUi.close(props.num);
props.controller.close(props.num);
}
const selectedGroup = ref('ui');

View File

@ -1,4 +1,4 @@
export { default as Achievement } from './achievement.vue';
// export { default as Achievement } from './achievement.vue';
export { default as BgmList } from './bgmList.vue';
export { default as Book } from './book.vue';
export { default as BookDetail } from './bookDetail.vue';

View File

@ -34,14 +34,10 @@ import {
import { GameUi } from '../controller';
import { formatSize } from '../utils';
import { logger } from '@motajs/common';
import { fixedUi } from '../preset/ui';
import { sleep } from 'mutate-animate';
import { IMountedVBind } from '../interface';
const props = defineProps<{
ui: GameUi;
num: number;
callback?: () => void;
}>();
const props = defineProps<IMountedVBind>();
const loading = ref(0);
const loaded = ref(0);
@ -76,8 +72,8 @@ onMounted(async () => {
loadDiv.style.opacity = '0';
Mota.require('@user/data-base').loading.emit('loaded');
await sleep(1000);
fixedUi.close(props.num);
fixedUi.open('start');
props.controller.close(props.num);
props.controller.open('start');
});
loadDiv = document.getElementById('load') as HTMLDivElement;
});

View File

@ -1,157 +0,0 @@
<template>
<div id="marked-enemy">
<Box
v-model:left="boxPos.left"
v-model:top="boxPos.top"
v-model:width="boxPos.width"
v-model:height="boxPos.height"
:resizable="true"
:dragable="true"
>
<Scroll class="box-scroll" :no-scroll="true">
<div class="marked-main">
<div class="marked-info">
<BoxAnimate
:id="enemy.id"
:width="24"
:height="24"
></BoxAnimate>
<span class="marked-name marked-item">{{
getName()
}}</span>
</div>
<span class="marked-damage marked-item"
>伤害:{{ format(info.damage) }}</span
>
<span class="marked-critical marked-item"
>临界:{{ format(info.critical) }}</span
>
<span class="marked-critical-damage marked-item"
>减伤:{{ format(info.criticalDam) }}</span
>
<span class="marked-def marked-item"
>{{ ratio }}防:{{ format(info.defDamage) }}</span
>
<div class="marked-button">
<span
class="marked-hide button-text"
@click.stop="hidden = true"
>隐藏盒子</span
>
<span
class="marked-cancel button-text"
@click.stop="unmarkEnemy(enemy.id)"
>取消标记</span
>
</div>
</div></Scroll
>
</Box>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue';
import { MarkInfo, unmarkEnemy } from '../mark';
import Box from '../components/box.vue';
import Scroll from '../components/scroll.vue';
import BoxAnimate from '../components/boxAnimate.vue';
import { GameUi } from '../controller';
import { fixedUi } from '../preset/ui';
const props = defineProps<{
num: number;
ui: GameUi;
enemy: MarkInfo<EnemyIds>;
}>();
interface BoxPos {
left: number;
top: number;
width: number;
height: number;
}
interface MarkedEnemy {
damage: number;
critical: number;
criticalDam: number;
defDamage: number;
}
const enemy = props.enemy;
const ratio = core.status.thisMap?.ratio ?? 1;
const format = core.formatBigNumber;
const boxPos = reactive<BoxPos>({
left: window.innerWidth - 300,
top: 100,
width: 200,
height: 150
});
const info = reactive<MarkedEnemy>({
damage: 0,
critical: 0,
criticalDam: 0,
defDamage: 0
});
const hidden = ref(false);
watch(hidden, n => {
if (n) fixedUi.close(props.num);
});
watch(enemy.update, update);
function update() {
info.damage = enemy.enemy.calDamage().damage;
const critical = enemy.enemy.calCritical()[0];
info.critical = critical?.atkDelta ?? 0;
info.criticalDam = critical?.delta ?? 0;
info.defDamage = enemy.enemy.calDefDamage(ratio).delta;
}
function getName() {
return enemy.enemy.enemy.name;
}
</script>
<style lang="less" scoped>
#marked-enemy {
width: 100%;
height: 100%;
}
.box-scroll {
height: 100%;
width: 100%;
}
.marked-main {
padding: 1vh 0;
background-color: #0009;
display: flex;
flex-direction: column;
overflow: hidden;
}
.marked-info {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.marked-item {
margin-left: 10%;
}
.marked-button {
align-self: center;
width: 80%;
display: flex;
flex-direction: row;
justify-content: space-around;
}
</style>

View File

@ -73,7 +73,7 @@
<script lang="ts" setup>
import { computed, onUnmounted, ref, shallowRef } from 'vue';
import { mainSetting } from '../preset/ui';
import { mainSetting } from '../preset/settingIns';
import {
MotaSetting,
MotaSettingItem,
@ -85,14 +85,9 @@ import { splitText } from '../utils';
import Scroll from '../components/scroll.vue';
import { isMobile } from '../use';
import { gameKey } from '@motajs/system-action';
import { GameUi } from '../controller';
import { mainUi } from '../preset/ui';
import { IMountedVBind } from '../interface';
const props = defineProps<{
info?: MotaSetting;
num: number;
ui: GameUi;
}>();
const props = defineProps<IMountedVBind>();
const setting = props.info ?? mainSetting;
const display = shallowRef<SettingDisplayInfo[]>([]);
@ -145,7 +140,7 @@ function click(key: string, index: number, item: MotaSettingItem) {
}
function exit() {
mainUi.close(props.num);
props.controller.close(props.num);
}
gameKey.use(props.ui.symbol);

View File

@ -169,18 +169,14 @@ import {
RightOutlined,
DoubleRightOutlined
} from '@ant-design/icons-vue';
import { splitText, tip } from '../utils';
import { splitText } from '../utils';
import Scroll from '../components/scroll.vue';
import BoxAnimate from '../components/boxAnimate.vue';
import { GameUi } from '../controller';
import { gameKey } from '@motajs/system-action';
import { mainUi } from '../preset/ui';
import { IMountedVBind } from '../interface';
import { tip } from '../use';
const props = defineProps<{
num: number;
ui: GameUi;
shopId: string;
}>();
const props = defineProps<IMountedVBind>();
const id = props.shopId;
const shop = core.status.shops[id] as ItemShopEvent;
@ -315,7 +311,7 @@ gameKey
function exit() {
if (bought) core.status.route.push('closeShop');
mainUi.close(props.num);
props.controller.close(props.num);
}
onMounted(async () => {

View File

@ -19,13 +19,11 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import skills from '../data/skill.json';
import { has } from '../utils';
import Column from '../components/colomn.vue';
import { mainUi } from '../preset/ui';
import { IMountedVBind } from '../interface';
import { isNil } from 'lodash-es';
const props = defineProps<{
num: number;
}>();
const props = defineProps<IMountedVBind>();
type Skills = keyof typeof skills;
@ -47,7 +45,7 @@ const content = computed(() => {
.map((v, i, a) => {
if (/^\d+\./.test(v)) return `${'&nbsp;'.repeat(12)}${v}`;
else if (
(has(a[i - 1]) &&
(!isNil(a[i - 1]) &&
v !== '<br>' &&
a[i - 1] === '<br>') ||
i === 0
@ -65,7 +63,7 @@ const content = computed(() => {
});
function exit() {
mainUi.close(props.num);
props.controller.close(props.num);
}
</script>

View File

@ -81,17 +81,14 @@
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
import Scroll from '../components/scroll.vue';
import { has, splitText, tip } from '../utils';
import { isMobile } from '../use';
import { splitText } from '../utils';
import { isMobile, tip } from '../use';
import { sleep } from 'mutate-animate';
import { gameKey } from '@motajs/system-action';
import { GameUi } from '../controller';
import { mainUi } from '../preset/ui';
import { IMountedVBind } from '../interface';
import { isNil } from 'lodash-es';
const props = defineProps<{
num: number;
ui: GameUi;
}>();
const props = defineProps<IMountedVBind>();
const skillTree = Mota.require('@user/legacy-plugin-data');
@ -179,7 +176,7 @@ const level = computed(() => {
});
function exit() {
mainUi.close(props.num);
props.controller.close(props.num);
}
function resize() {
@ -298,7 +295,7 @@ function selectChapter(delta: number) {
const now = chapterList.indexOf(chapter.value);
const to = now + delta;
if (has(chapterList[to]) && flags.chapter > to) {
if (!isNil(chapterList[to]) && flags.chapter > to) {
selected.value = s[chapterList[to]][0].index;
chapter.value = chapterList[to];
update.value = !update.value;

View File

@ -64,19 +64,14 @@ import {
FullscreenExitOutlined
} from '@ant-design/icons-vue';
import { sleep } from 'mutate-animate';
import { doByInterval } from '../utils';
import { triggerFullscreen } from '../utils';
import { doByInterval, triggerFullscreen } from '../utils';
import { isMobile } from '../use';
import { GameUi } from '../controller';
import { gameKey } from '@motajs/system-action';
import { mainUi } from '../preset/ui';
import { mainSetting } from '../preset/ui';
import { mainSetting } from '../preset/settingIns';
import { mat4 } from 'gl-matrix';
import { IMountedVBind } from '../interface';
const props = defineProps<{
num: number;
ui: GameUi;
}>();
const props = defineProps<IMountedVBind>();
const bg = core.material.images.images['bg.webp'];
@ -158,7 +153,7 @@ async function clickStartButton(id: string) {
}
if (id === 'replay') core.chooseReplayFile();
if (id === 'achievement') {
mainUi.open('achievement');
props.controller.open('achievement');
}
}

View File

@ -85,8 +85,8 @@
<span>{{
selected === 'none'
? '永久道具'
: (getClsName(all[selected].cls as ItemMode) ??
'永久道具')
: getClsName(all[selected].cls as ItemMode) ??
'永久道具'
}}</span>
</div>
</div>
@ -113,17 +113,14 @@ import Scroll from '../components/scroll.vue';
import BoxAnimate from '../components/boxAnimate.vue';
import { getClsName, getItems } from '../tools/toolbox';
import { isMobile } from '../use';
import { type, has } from '../utils';
import { type } from '../utils';
import { hyper } from 'mutate-animate';
import { message } from 'ant-design-vue';
import { GameUi } from '../controller';
import { gameKey } from '@motajs/system-action';
import { mainUi } from '../preset/ui';
import { IMountedVBind } from '../interface';
import { isNil } from 'lodash-es';
const props = defineProps<{
num: number;
ui: GameUi;
}>();
const props = defineProps<IMountedVBind>();
type ItemMode = 'tools' | 'constants';
type ShowItemIds = ItemIdOf<'constants' | 'tools'> | 'none';
@ -146,7 +143,7 @@ watch(index, n => {
});
watch(mode, n => {
if (!has(items[n][index.value])) {
if (isNil(items[n][index.value])) {
selected.value = 'none';
return;
}
@ -172,17 +169,17 @@ async function select(id: ShowItemIds, nouse: boolean = false) {
}
function exit() {
mainUi.close(props.num);
props.controller.close(props.num);
}
function use(id: ShowItemIds) {
if (id === 'none') return;
if (core.canUseItem(id)) {
const hold = mainUi.holdOn();
const hold = props.controller.holdOn();
exit();
nextTick(() => {
core.tryUseItem(id, false, () => {
if (mainUi.stack.length === 0) {
if (props.controller.stack.length === 0) {
hold.end(core.status.event.id !== 'toolbox');
}
});
@ -196,10 +193,10 @@ function use(id: ShowItemIds) {
}
async function toEquip() {
mainUi.holdOn();
props.controller.holdOn();
exit();
nextTick(() => {
mainUi.open('equipbox');
props.controller.open('equipbox');
});
}

View File

@ -0,0 +1,47 @@
import { KeyCode } from '@motajs/client-base';
import { KeyboardEmits, Keyboard, isAssist } from '@motajs/system-action';
import { mainUi, fixedUi } from './preset/uiIns';
/**
* 唤起虚拟键盘,并获取到一次按键操作
* @param emitAssist 是否可以获取辅助按键,为true时,如果按下辅助按键,那么会立刻返回该按键,
* 否则会视为开关辅助按键
* @param assist 初始化的辅助按键
*/
export function getVitualKeyOnce(
emitAssist: boolean = false,
assist: number = 0,
emittable: KeyCode[] = []
): Promise<KeyboardEmits> {
// todo: 正确触发后删除监听器
return new Promise(res => {
const key = Keyboard.get('full')!;
key.withAssist(assist);
const id = mainUi.open('virtualKey', { keyboard: key });
key.on('emit', (item, assist, _index, ev) => {
ev.preventDefault();
if (emitAssist) {
if (emittable.length === 0 || emittable.includes(item.key)) {
res({ key: item.key, assist: 0 });
key.disposeScope();
mainUi.close(id);
}
} else {
if (
!isAssist(item.key) &&
(emittable.length === 0 || emittable.includes(item.key))
) {
res({ key: item.key, assist });
key.disposeScope();
mainUi.close(id);
}
}
});
});
}
export function openDanmakuPoster() {
if (!fixedUi.hasName('danmakuEditor')) {
fixedUi.open('danmakuEditor');
}
}

View File

@ -1,8 +1,23 @@
import { sleep } from 'mutate-animate';
import { tip } from './utils';
import { message } from 'ant-design-vue';
import { MessageApi } from 'ant-design-vue/lib/message';
export default function init() {
return { useDrag, useWheel, useUp, isMobile };
message.config({
maxCount: 3
});
export function tip(
type: Exclude<keyof MessageApi, 'open' | 'config' | 'destroy'>,
text: string
) {
message[type]({
content: text,
class: 'antdv-message'
});
}
let num = 0;
export function requireUniqueSymbol() {
return num++;
}
type DragFn = (x: number, y: number, e: MouseEvent | TouchEvent) => void;

View File

@ -1,13 +1,10 @@
import { message } from 'ant-design-vue';
import { MessageApi } from 'ant-design-vue/lib/message';
import { isNil } from 'lodash-es';
import { Animation, sleep, TimingFn } from 'mutate-animate';
import { Ref, ref } from 'vue';
import { EVENT_KEY_CODE_MAP, KeyCode } from '@motajs/client-base';
import { KeyCode } from '@motajs/client-base';
import axios from 'axios';
import { decompressFromBase64 } from 'lz-string';
import { Keyboard, KeyboardEmits, isAssist } from '@motajs/system-action';
import { fixedUi, mainUi } from './preset/ui';
import { logger } from '@motajs/common';
type CanParseCss = keyof {
@ -18,25 +15,6 @@ type CanParseCss = keyof {
: never]: CSSStyleDeclaration[P];
};
export default function init() {
return {
has,
getDamageColor,
parseCss,
tip,
changeLocalStorage,
swapChapter
};
}
/**
* 判定一个值是否不是undefined或null
* @param value 要判断的值
*/
export function has<T>(value: T): value is NonNullable<T> {
return !isNil(value);
}
/**
* 根据伤害大小获取颜色
* @param damage 伤害大小
@ -68,14 +46,6 @@ export function setCanvasSize(
canvas.style.height = `${h}px`;
}
/**
* 获取事件中的keycode对应的键
* @param key 要获取的键
*/
export function keycode(key: number) {
return EVENT_KEY_CODE_MAP[key];
}
/**
* 解析css字符串为CSSStyleDeclaration对象
* @param css 要解析的css字符串
@ -195,7 +165,7 @@ export function type(
const all = toShow.length;
const fn = (time: number) => {
if (!has(time)) return;
if (isNil(time)) return;
const now = ani.x;
content.value = toShow.slice(0, Math.floor(now));
if (Math.floor(now) === all) {
@ -213,19 +183,6 @@ export function type(
return content;
}
message.config({
maxCount: 3
});
export function tip(
type: Exclude<keyof MessageApi, 'open' | 'config' | 'destroy'>,
text: string
) {
message[type]({
content: text,
class: 'antdv-message'
});
}
/**
* 设置文字分段换行等
* @param str 文字
@ -235,7 +192,7 @@ export function splitText(str: string[]) {
.map((v, i, a) => {
if (/^\d+\./.test(v)) return `${'&nbsp;'.repeat(12)}${v}`;
else if (
(has(a[i - 1]) && v !== '<br>' && a[i - 1] === '<br>') ||
(!isNil(a[i - 1]) && v !== '<br>' && a[i - 1] === '<br>') ||
i === 0
) {
return `${'&nbsp;'.repeat(8)}${v}`;
@ -339,25 +296,6 @@ export function ensureArray<T>(arr: T): T extends any[] ? T : T[] {
return arr instanceof Array ? arr : [arr];
}
/**
* 删除数组内的某个项,返回删除后的数组
* @param arr 要操作的数组
* @param ele 要删除的项
*/
export function deleteWith<T>(arr: T[], ele: T): T[] {
const index = arr.indexOf(ele);
if (index === -1) return arr;
arr.splice(index, 1);
return arr;
}
export function spliceBy<T>(arr: T[], from: T): T[] {
const index = arr.indexOf(from);
if (index === -1) return arr;
arr.splice(index);
return arr;
}
export async function triggerFullscreen(full: boolean) {
const { maxGameScale } = Mota.require('@user/data-utils');
if (!!document.fullscreenElement && !full) {
@ -382,20 +320,6 @@ export async function triggerFullscreen(full: boolean) {
}
}
/**
* 根据布尔值数组转换成一个二进制数
* @param arr 要转换的布尔值数组
*/
export function generateBinary(arr: boolean[]) {
let num = 0;
arr.forEach((v, i) => {
if (v) {
num |= 1 << i;
}
});
return num;
}
/**
* 获得某个状态的中文名
* @param name 要获取的属性名
@ -423,50 +347,6 @@ export function getStatusLabel(name: string) {
);
}
export function flipBinary(num: number, col: number) {
const n = 1 << col;
if (num & n) return num & ~n;
else return num | n;
}
/**
* 唤起虚拟键盘,并获取到一次按键操作
* @param emitAssist 是否可以获取辅助按键,为true时,如果按下辅助按键,那么会立刻返回该按键,
* 否则会视为开关辅助按键
* @param assist 初始化的辅助按键
*/
export function getVitualKeyOnce(
emitAssist: boolean = false,
assist: number = 0,
emittable: KeyCode[] = []
): Promise<KeyboardEmits> {
// todo: 正确触发后删除监听器
return new Promise(res => {
const key = Keyboard.get('full')!;
key.withAssist(assist);
const id = mainUi.open('virtualKey', { keyboard: key });
key.on('emit', (item, assist, _index, ev) => {
ev.preventDefault();
if (emitAssist) {
if (emittable.length === 0 || emittable.includes(item.key)) {
res({ key: item.key, assist: 0 });
key.disposeScope();
mainUi.close(id);
}
} else {
if (
!isAssist(item.key) &&
(emittable.length === 0 || emittable.includes(item.key))
) {
res({ key: item.key, assist });
key.disposeScope();
mainUi.close(id);
}
}
});
});
}
export function formatSize(size: number) {
return size < 1 << 10
? `${size.toFixed(2)}B`
@ -477,17 +357,6 @@ export function formatSize(size: number) {
: `${(size / (1 << 30)).toFixed(2)}GB`;
}
let num = 0;
export function requireUniqueSymbol() {
return num++;
}
export function openDanmakuPoster() {
if (!fixedUi.hasName('danmakuEditor')) {
fixedUi.open('danmakuEditor');
}
}
export function getIconHeight(icon: AllIds | 'hero') {
if (icon === 'hero') {
if (core.isPlaying()) {

View File

@ -1,415 +0,0 @@
import EventEmitter from 'eventemitter3';
import {
FloorLayer,
ILayerGroupRenderExtends,
ILayerRenderExtends,
Layer,
LayerGroup,
LayerMovingRenderable
} from './layer';
import { texture } from './cache';
import { sleep } from 'mutate-animate';
import { RenderAdapter } from '@motajs/render-core';
const { hook } = Mota.require('@user/data-base');
hook.on('setBlock', (x, y, floor, block) => {
const isNow = floor === core.status.floorId;
LayerGroupFloorBinder.activedBinder.forEach(v => {
if (floor === v.floor || (isNow && v.bindThisFloor)) {
v.setBlock('event', block, x, y);
}
});
LayerFloorBinder.listenedBinder.forEach(v => {
if (v.layer.layer === 'event') {
if (v.floor === floor || (isNow && v.bindThisFloor)) {
v.setBlock(block, x, y);
}
}
});
});
hook.on('changingFloor', floor => {
// 潜在隐患:如果putRenderData改成异步,那么会变成两帧后才能真正刷新并渲染
// 考虑到楼层转换一般不会同时执行很多次,因此这里改为立刻更新
LayerGroupFloorBinder.activedBinder.forEach(v => {
if (v.bindThisFloor) v.updateBindData();
v.emit('floorChange', floor);
});
LayerFloorBinder.listenedBinder.forEach(v => {
if (v.bindThisFloor) v.updateBindData();
});
});
hook.on('setBgFgBlock', (name, number, x, y, floor) => {
const isNow = floor === core.status.floorId;
LayerGroupFloorBinder.activedBinder.forEach(v => {
if (floor === v.floor || (isNow && v.bindThisFloor)) {
v.setBlock(name, number, x, y);
}
});
LayerFloorBinder.listenedBinder.forEach(v => {
if (v.layer.layer === name) {
if (v.floor === floor || (isNow && v.bindThisFloor)) {
v.setBlock(number, x, y);
}
}
});
});
interface LayerGroupBinderEvent {
update: [floor: FloorIds];
setBlock: [x: number, y: number, floor: FloorIds, block: AllNumbers];
floorChange: [floor: FloorIds];
}
/**
* 楼层绑定拓展,用于LayerGroup,将楼层数据传输到渲染系统。
* 添加后,会自动在LayerGroup包含的子Layer上添加LayerFloorBinder拓展,用于后续处理。
* 当移除这个拓展时,其附属的所有子拓展也会一并被移除。
*/
export class LayerGroupFloorBinder
extends EventEmitter<LayerGroupBinderEvent>
implements ILayerGroupRenderExtends
{
id: string = 'floor-binder';
bindThisFloor: boolean = true;
floor?: FloorIds;
group!: LayerGroup;
/** 附属的子LayerFloorBinder拓展 */
layerBinders: Set<LayerFloorBinder> = new Set();
private needUpdate: boolean = false;
static activedBinder: Set<LayerGroupFloorBinder> = new Set();
/**
* 绑定楼层为当前楼层,并跟随变化
*/
bindThis() {
this.floor = void 0;
this.bindThisFloor = true;
this.layerBinders.forEach(v => v.bindThis());
this.updateBind();
}
/**
* 绑定楼层为指定楼层
* @param floorId 楼层id
*/
bindFloor(floorId: FloorIds) {
this.bindThisFloor = false;
this.floor = floorId;
this.layerBinders.forEach(v => v.bindFloor(floorId));
this.updateBind();
}
/**
* 在下一帧进行绑定数据更新
*/
updateBind() {
if (this.needUpdate || !this.group) return;
this.needUpdate = true;
this.group.requestBeforeFrame(() => {
this.needUpdate = false;
this.updateBindData();
});
}
/**
* 立刻进行数据绑定更新
*/
updateBindData() {
this.layerBinders.forEach(v => {
v.updateBindData();
});
const floor = this.getFloor();
this.emit('update', floor);
}
getFloor() {
return this.bindThisFloor ? core.status.floorId : this.floor!;
}
/**
* 设置图块
*/
setBlock(layer: FloorLayer, block: AllNumbers, x: number, y: number) {
const ex = this.group
.getLayer(layer)
?.getExtends('floor-binder') as LayerFloorBinder;
if (!ex) return;
ex.setBlock(block, x, y);
const floor = this.bindThisFloor ? core.status.floorId : this.floor!;
this.emit('setBlock', x, y, floor, block);
}
checkLayerExtends(layer: Layer) {
const ex = layer.getExtends('floor-binder');
if (!ex) {
const extend = new LayerFloorBinder(this);
layer.extends(extend);
this.layerBinders.add(extend);
} else {
if (ex instanceof LayerFloorBinder) {
ex.setParent(this);
this.layerBinders.add(ex);
}
}
}
awake(group: LayerGroup) {
this.group = group;
for (const layer of group.layers.values()) {
this.checkLayerExtends(layer);
}
LayerGroupFloorBinder.activedBinder.add(this);
}
onLayerAdd(_group: LayerGroup, layer: Layer): void {
this.checkLayerExtends(layer);
}
onDestroy(group: LayerGroup) {
LayerGroupFloorBinder.activedBinder.delete(this);
group.layers.forEach(v => {
v.removeExtends('floor-binder');
});
this.removeAllListeners();
}
}
/**
* 楼层绑定拓展,用于Layer的楼层渲染。
* 注意,如果目标Layer是LayerGroup的子元素,那么会自动检测父元素是否包含LayerGroupFloorBinder拓展,
* 如果包含,那么会自动将此拓展附加至父元素的拓展。当父元素的拓展被移除时,此拓展也会一并被移除。
*/
export class LayerFloorBinder implements ILayerRenderExtends {
id: string = 'floor-binder';
parent?: LayerGroupFloorBinder;
layer!: Layer;
bindThisFloor: boolean = true;
floor?: FloorIds;
static listenedBinder: Set<LayerFloorBinder> = new Set();
private needUpdate: boolean = false;
constructor(parent?: LayerGroupFloorBinder) {
this.parent = parent;
}
/**
* 绑定楼层为当前楼层,并跟随变化
*/
bindThis() {
this.floor = void 0;
this.bindThisFloor = true;
this.updateBind();
}
/**
* 绑定楼层为指定楼层
* @param floorId 楼层id
*/
bindFloor(floorId: FloorIds) {
this.bindThisFloor = false;
this.floor = floorId;
this.updateBind();
}
getFloor() {
return this.bindThisFloor ? core.status.floorId : this.floor!;
}
/**
* 设置这个拓展附属至的父拓展(LayerGroupFloorBinder拓展)
* @param parent 父拓展
*/
setParent(parent?: LayerGroupFloorBinder) {
this.parent = parent;
this.checkListen();
}
private checkListen() {
if (this.parent) LayerFloorBinder.listenedBinder.delete(this);
else LayerFloorBinder.listenedBinder.add(this);
}
/**
* 在下一帧进行绑定数据更新
*/
updateBind() {
if (this.needUpdate) return;
this.needUpdate = true;
this.layer.requestBeforeFrame(() => {
this.needUpdate = false;
this.updateBindData();
});
}
/**
* 设置图块
*/
setBlock(block: AllNumbers, x: number, y: number) {
this.layer.putRenderData([block], 1, x, y);
}
/**
* 立刻更新绑定数据,而非下一帧
*/
updateBindData() {
const floor = this.getFloor();
if (!floor) return;
core.extractBlocks(floor);
const map = core.status.maps[floor];
this.layer.setMapSize(map.width, map.height);
const image = core.status.maps[this.getFloor()].images;
if (this.layer.layer === 'event') {
const m = map.map;
this.layer.putRenderData(m.flat(), map.width, 0, 0);
} else {
const m = core.maps._getBgFgMapArray(this.layer.layer!, floor);
this.layer.putRenderData(m.flat(), map.width, 0, 0);
}
if (this.layer.layer === 'bg') {
// 别忘了背景图块
this.layer.setBackground(texture.idNumberMap[map.defaultGround]);
}
const toDraw = image?.filter(v => v.canvas === this.layer.layer);
this.layer.setFloorImage(toDraw ?? []);
}
awake(layer: Layer) {
this.layer = layer;
if (!this.parent) {
const group = layer.parent;
if (group instanceof LayerGroup) {
const ex = group.getExtends('floor-binder');
if (ex instanceof LayerGroupFloorBinder) {
ex.checkLayerExtends(layer);
this.parent = ex;
}
}
}
this.checkListen();
}
onDestroy(_layer: Layer) {
LayerFloorBinder.listenedBinder.delete(this);
this.parent?.layerBinders.delete(this);
}
}
interface DoorAnimateRenderable {
renderable: LayerMovingRenderable;
count: number;
perTime: number;
}
export class LayerDoorAnimate implements ILayerRenderExtends {
id: string = 'door-animate';
layer!: Layer;
private moving: Set<LayerMovingRenderable> = new Set();
private getRenderable(block: Block): DoorAnimateRenderable | null {
const { x, y, id } = block;
const renderable = texture.getRenderable(id);
if (!renderable) return null;
const image = renderable.autotile
? renderable.image[0]
: renderable.image;
const time = block.event.doorInfo?.time ?? 160;
const frame = renderable.render.length;
const perTime = time / frame;
const data: LayerMovingRenderable = {
x,
y,
zIndex: y,
image,
autotile: false,
animate: 0,
frame,
bigImage: false,
render: renderable.render,
alpha: 1
};
return { renderable: data, count: frame, perTime };
}
/**
* 开门
* @param block 图块信息
*/
async openDoor(block: Block) {
const renderable = this.getRenderable(block);
if (!renderable) return Promise.reject();
const { renderable: data, count: frame, perTime } = renderable;
data.animate = 0;
this.moving.add(data);
this.layer.requestUpdateMoving();
let now = 0;
while (now < frame) {
await sleep(perTime);
data.animate = ++now;
this.layer.update(this.layer);
}
this.moving.delete(data);
this.layer.requestUpdateMoving();
return Promise.resolve();
}
/**
* 关门
* @param block 图块信息
*/
async closeDoor(block: Block) {
const renderable = this.getRenderable(block);
if (!renderable) return Promise.reject();
const { renderable: data, count: frame, perTime } = renderable;
data.animate = frame - 1;
this.moving.add(data);
this.layer.requestUpdateMoving();
let now = 0;
while (now >= 0) {
await sleep(perTime);
data.animate = --now;
this.layer.update(this.layer);
}
this.moving.delete(data);
this.layer.requestUpdateMoving();
return Promise.resolve();
}
awake(layer: Layer) {
this.layer = layer;
doorAdapter.add(this);
}
onMovingUpdate(_layer: Layer, renderable: LayerMovingRenderable[]): void {
renderable.push(...this.moving);
}
onDestroy(_layer: Layer): void {
doorAdapter.remove(this);
}
}
const doorAdapter = new RenderAdapter<LayerDoorAnimate>('door-animate');
doorAdapter.receive('openDoor', (item, block: Block) => {
return item.openDoor(block);
});
doorAdapter.receive('closeDoor', (item, block: Block) => {
return item.closeDoor(block);
});

View File

@ -2,7 +2,6 @@ export * from './animate';
export * from './block';
export * from './cache';
export * from './camera';
export * from './floor';
export * from './frame';
export * from './graphics';
export * from './hero';

View File

@ -8,11 +8,11 @@ import {
RenderAdapter
} from '@motajs/render-core';
import { logger } from '@motajs/common';
import { TimingFn } from 'mutate-animate';
import { sleep, TimingFn } from 'mutate-animate';
import { RenderableData, texture } from './cache';
import { BlockCacher, CanvasCacheItem, ICanvasCacheItem } from './block';
import { LayerFloorBinder, LayerGroupFloorBinder } from './floor';
import { IAnimateFrame, renderEmits } from './frame';
import { EventEmitter } from 'eventemitter3';
export interface ILayerGroupRenderExtends {
/** 拓展的唯一标识符 */
@ -1550,3 +1550,406 @@ export class Layer extends Container<ELayerEvent> {
}
const layerAdapter = new RenderAdapter<Layer>('layer');
const { hook } = Mota.require('@user/data-base');
hook.on('setBlock', (x, y, floor, block) => {
const isNow = floor === core.status.floorId;
LayerGroupFloorBinder.activedBinder.forEach(v => {
if (floor === v.floor || (isNow && v.bindThisFloor)) {
v.setBlock('event', block, x, y);
}
});
LayerFloorBinder.listenedBinder.forEach(v => {
if (v.layer.layer === 'event') {
if (v.floor === floor || (isNow && v.bindThisFloor)) {
v.setBlock(block, x, y);
}
}
});
});
hook.on('changingFloor', floor => {
// 潜在隐患:如果putRenderData改成异步,那么会变成两帧后才能真正刷新并渲染
// 考虑到楼层转换一般不会同时执行很多次,因此这里改为立刻更新
LayerGroupFloorBinder.activedBinder.forEach(v => {
if (v.bindThisFloor) v.updateBindData();
v.emit('floorChange', floor);
});
LayerFloorBinder.listenedBinder.forEach(v => {
if (v.bindThisFloor) v.updateBindData();
});
});
hook.on('setBgFgBlock', (name, number, x, y, floor) => {
const isNow = floor === core.status.floorId;
LayerGroupFloorBinder.activedBinder.forEach(v => {
if (floor === v.floor || (isNow && v.bindThisFloor)) {
v.setBlock(name, number, x, y);
}
});
LayerFloorBinder.listenedBinder.forEach(v => {
if (v.layer.layer === name) {
if (v.floor === floor || (isNow && v.bindThisFloor)) {
v.setBlock(number, x, y);
}
}
});
});
interface LayerGroupBinderEvent {
update: [floor: FloorIds];
setBlock: [x: number, y: number, floor: FloorIds, block: AllNumbers];
floorChange: [floor: FloorIds];
}
/**
* 楼层绑定拓展,用于LayerGroup,将楼层数据传输到渲染系统。
* 添加后,会自动在LayerGroup包含的子Layer上添加LayerFloorBinder拓展,用于后续处理。
* 当移除这个拓展时,其附属的所有子拓展也会一并被移除。
*/
export class LayerGroupFloorBinder
extends EventEmitter<LayerGroupBinderEvent>
implements ILayerGroupRenderExtends
{
id: string = 'floor-binder';
bindThisFloor: boolean = true;
floor?: FloorIds;
group!: LayerGroup;
/** 附属的子LayerFloorBinder拓展 */
layerBinders: Set<LayerFloorBinder> = new Set();
private needUpdate: boolean = false;
static activedBinder: Set<LayerGroupFloorBinder> = new Set();
/**
* 绑定楼层为当前楼层,并跟随变化
*/
bindThis() {
this.floor = void 0;
this.bindThisFloor = true;
this.layerBinders.forEach(v => v.bindThis());
this.updateBind();
}
/**
* 绑定楼层为指定楼层
* @param floorId 楼层id
*/
bindFloor(floorId: FloorIds) {
this.bindThisFloor = false;
this.floor = floorId;
this.layerBinders.forEach(v => v.bindFloor(floorId));
this.updateBind();
}
/**
* 在下一帧进行绑定数据更新
*/
updateBind() {
if (this.needUpdate || !this.group) return;
this.needUpdate = true;
this.group.requestBeforeFrame(() => {
this.needUpdate = false;
this.updateBindData();
});
}
/**
* 立刻进行数据绑定更新
*/
updateBindData() {
this.layerBinders.forEach(v => {
v.updateBindData();
});
const floor = this.getFloor();
this.emit('update', floor);
}
getFloor() {
return this.bindThisFloor ? core.status.floorId : this.floor!;
}
/**
* 设置图块
*/
setBlock(layer: FloorLayer, block: AllNumbers, x: number, y: number) {
const ex = this.group
.getLayer(layer)
?.getExtends('floor-binder') as LayerFloorBinder;
if (!ex) return;
ex.setBlock(block, x, y);
const floor = this.bindThisFloor ? core.status.floorId : this.floor!;
this.emit('setBlock', x, y, floor, block);
}
checkLayerExtends(layer: Layer) {
const ex = layer.getExtends('floor-binder');
if (!ex) {
const extend = new LayerFloorBinder(this);
layer.extends(extend);
this.layerBinders.add(extend);
} else {
if (ex instanceof LayerFloorBinder) {
ex.setParent(this);
this.layerBinders.add(ex);
}
}
}
awake(group: LayerGroup) {
this.group = group;
for (const layer of group.layers.values()) {
this.checkLayerExtends(layer);
}
LayerGroupFloorBinder.activedBinder.add(this);
}
onLayerAdd(_group: LayerGroup, layer: Layer): void {
this.checkLayerExtends(layer);
}
onDestroy(group: LayerGroup) {
LayerGroupFloorBinder.activedBinder.delete(this);
group.layers.forEach(v => {
v.removeExtends('floor-binder');
});
this.removeAllListeners();
}
}
/**
* 楼层绑定拓展,用于Layer的楼层渲染。
* 注意,如果目标Layer是LayerGroup的子元素,那么会自动检测父元素是否包含LayerGroupFloorBinder拓展,
* 如果包含,那么会自动将此拓展附加至父元素的拓展。当父元素的拓展被移除时,此拓展也会一并被移除。
*/
export class LayerFloorBinder implements ILayerRenderExtends {
id: string = 'floor-binder';
parent?: LayerGroupFloorBinder;
layer!: Layer;
bindThisFloor: boolean = true;
floor?: FloorIds;
static listenedBinder: Set<LayerFloorBinder> = new Set();
private needUpdate: boolean = false;
constructor(parent?: LayerGroupFloorBinder) {
this.parent = parent;
}
/**
* 绑定楼层为当前楼层,并跟随变化
*/
bindThis() {
this.floor = void 0;
this.bindThisFloor = true;
this.updateBind();
}
/**
* 绑定楼层为指定楼层
* @param floorId 楼层id
*/
bindFloor(floorId: FloorIds) {
this.bindThisFloor = false;
this.floor = floorId;
this.updateBind();
}
getFloor() {
return this.bindThisFloor ? core.status.floorId : this.floor!;
}
/**
* 设置这个拓展附属至的父拓展(LayerGroupFloorBinder拓展)
* @param parent 父拓展
*/
setParent(parent?: LayerGroupFloorBinder) {
this.parent = parent;
this.checkListen();
}
private checkListen() {
if (this.parent) LayerFloorBinder.listenedBinder.delete(this);
else LayerFloorBinder.listenedBinder.add(this);
}
/**
* 在下一帧进行绑定数据更新
*/
updateBind() {
if (this.needUpdate) return;
this.needUpdate = true;
this.layer.requestBeforeFrame(() => {
this.needUpdate = false;
this.updateBindData();
});
}
/**
* 设置图块
*/
setBlock(block: AllNumbers, x: number, y: number) {
this.layer.putRenderData([block], 1, x, y);
}
/**
* 立刻更新绑定数据,而非下一帧
*/
updateBindData() {
const floor = this.getFloor();
if (!floor) return;
core.extractBlocks(floor);
const map = core.status.maps[floor];
this.layer.setMapSize(map.width, map.height);
const image = core.status.maps[this.getFloor()].images;
if (this.layer.layer === 'event') {
const m = map.map;
this.layer.putRenderData(m.flat(), map.width, 0, 0);
} else {
const m = core.maps._getBgFgMapArray(this.layer.layer!, floor);
this.layer.putRenderData(m.flat(), map.width, 0, 0);
}
if (this.layer.layer === 'bg') {
// 别忘了背景图块
this.layer.setBackground(texture.idNumberMap[map.defaultGround]);
}
const toDraw = image?.filter(v => v.canvas === this.layer.layer);
this.layer.setFloorImage(toDraw ?? []);
}
awake(layer: Layer) {
this.layer = layer;
if (!this.parent) {
const group = layer.parent;
if (group instanceof LayerGroup) {
const ex = group.getExtends('floor-binder');
if (ex instanceof LayerGroupFloorBinder) {
ex.checkLayerExtends(layer);
this.parent = ex;
}
}
}
this.checkListen();
}
onDestroy(_layer: Layer) {
LayerFloorBinder.listenedBinder.delete(this);
this.parent?.layerBinders.delete(this);
}
}
interface DoorAnimateRenderable {
renderable: LayerMovingRenderable;
count: number;
perTime: number;
}
export class LayerDoorAnimate implements ILayerRenderExtends {
id: string = 'door-animate';
layer!: Layer;
private moving: Set<LayerMovingRenderable> = new Set();
private getRenderable(block: Block): DoorAnimateRenderable | null {
const { x, y, id } = block;
const renderable = texture.getRenderable(id);
if (!renderable) return null;
const image = renderable.autotile
? renderable.image[0]
: renderable.image;
const time = block.event.doorInfo?.time ?? 160;
const frame = renderable.render.length;
const perTime = time / frame;
const data: LayerMovingRenderable = {
x,
y,
zIndex: y,
image,
autotile: false,
animate: 0,
frame,
bigImage: false,
render: renderable.render,
alpha: 1
};
return { renderable: data, count: frame, perTime };
}
/**
* 开门
* @param block 图块信息
*/
async openDoor(block: Block) {
const renderable = this.getRenderable(block);
if (!renderable) return Promise.reject();
const { renderable: data, count: frame, perTime } = renderable;
data.animate = 0;
this.moving.add(data);
this.layer.requestUpdateMoving();
let now = 0;
while (now < frame) {
await sleep(perTime);
data.animate = ++now;
this.layer.update(this.layer);
}
this.moving.delete(data);
this.layer.requestUpdateMoving();
return Promise.resolve();
}
/**
* 关门
* @param block 图块信息
*/
async closeDoor(block: Block) {
const renderable = this.getRenderable(block);
if (!renderable) return Promise.reject();
const { renderable: data, count: frame, perTime } = renderable;
data.animate = frame - 1;
this.moving.add(data);
this.layer.requestUpdateMoving();
let now = 0;
while (now >= 0) {
await sleep(perTime);
data.animate = --now;
this.layer.update(this.layer);
}
this.moving.delete(data);
this.layer.requestUpdateMoving();
return Promise.resolve();
}
awake(layer: Layer) {
this.layer = layer;
doorAdapter.add(this);
}
onMovingUpdate(_layer: Layer, renderable: LayerMovingRenderable[]): void {
renderable.push(...this.moving);
}
onDestroy(_layer: Layer): void {
doorAdapter.remove(this);
}
}
const doorAdapter = new RenderAdapter<LayerDoorAnimate>('door-animate');
doorAdapter.receive('openDoor', (item, block: Block) => {
return item.openDoor(block);
});
doorAdapter.receive('closeDoor', (item, block: Block) => {
return item.closeDoor(block);
});

View File

@ -1,7 +1,7 @@
import { RenderAdapter } from '@motajs/render-core';
import { HeroRenderer } from './hero';
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
import { LayerGroupFloorBinder } from './floor';
import { LayerGroupFloorBinder } from './layer';
import { hyper, TimingFn } from 'mutate-animate';
export class FloorViewport implements ILayerGroupRenderExtends {

View File

@ -4,7 +4,7 @@ import {
generateBinary,
keycode,
spliceBy
} from '@motajs/legacy-ui';
} from '@motajs/legacy-common';
import { EventEmitter } from 'eventemitter3';
import { isNil } from 'lodash-es';

View File

@ -1,8 +1,12 @@
import { EventEmitter, Listener } from '@motajs/legacy-common';
import {
EventEmitter,
Listener,
deleteWith,
flipBinary
} from '@motajs/legacy-common';
import { KeyCode } from '@motajs/client-base';
import { gameKey } from './hotkey';
import { unwarpBinary } from './hotkey';
import { deleteWith, flipBinary } from '@motajs/legacy-ui';
import { cloneDeep } from 'lodash-es';
import { shallowReactive } from 'vue';

View File

@ -24,6 +24,14 @@ const aliases = glob.sync('packages/*/src').map((srcPath) => {
};
});
const aliasesUser = glob.sync('packages-user/*/src').map((srcPath) => {
const packageName = path.basename(path.dirname(srcPath));
return {
find: `@user/${packageName}`,
replacement: path.resolve(__dirname, srcPath),
};
});
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
@ -51,7 +59,8 @@ export default defineConfig({
base: `./`,
resolve: {
alias: [
...aliases
...aliases,
...aliasesUser
]
},
build: {