chore: 删除样板不需要的 UI

This commit is contained in:
unanmed 2025-09-30 13:25:29 +08:00
parent 6c75fa507c
commit fbecb5f8e4
36 changed files with 16 additions and 4583 deletions

View File

@ -1,6 +1,5 @@
import { KeyCode } from '@motajs/client-base';
import { gameKey, HotkeyJSON } from '@motajs/system-action';
import { hovered, mainUi, openDanmakuPoster } from '@motajs/legacy-ui';
import { GameStorage } from '@motajs/legacy-system';
export const mainScope = Symbol.for('@key_main');
@ -92,16 +91,6 @@ gameKey
name: '浏览地图_2',
defaults: KeyCode.PageDown
})
.register({
id: 'skillTree',
name: '技能树',
defaults: KeyCode.KeyJ
})
.register({
id: 'desc',
name: '百科全书',
defaults: KeyCode.KeyH
})
//#region 功能按键
.group('function', '功能按键')
.register({
@ -139,27 +128,6 @@ gameKey
name: '轻按_2',
defaults: KeyCode.Digit7
})
.register({
id: 'mark',
name: '标记怪物',
defaults: KeyCode.KeyM
})
.register({
id: 'special',
name: '鼠标位置怪物属性',
defaults: KeyCode.KeyE
})
.register({
id: 'critical',
name: '鼠标位置怪物临界',
defaults: KeyCode.KeyC
})
.register({
id: 'danmaku',
name: '发送弹幕',
defaults: KeyCode.KeyA,
ctrl: true
})
.register({
id: 'quickEquip_1',
name: '切换/保存套装_1',
@ -524,12 +492,6 @@ gameKey
.realize('shop', () => {
core.openQuickShop(true);
})
.realize('skillTree', () => {
core.useItem('skill1', true);
})
.realize('desc', () => {
core.useItem('I560', true);
})
.realize('undo', () => {
core.doSL('autoSave', 'load');
})
@ -542,31 +504,6 @@ gameKey
.realize('getNext', () => {
core.getNextItem();
})
.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);
}
})
.realize('special', () => {
if (hovered) {
const { x, y } = hovered;
const enemy = core.status.thisMap.enemy.get(x, y);
if (enemy) mainUi.open('fixedDetail', { panel: 'special' });
}
})
.realize('critical', () => {
if (hovered) {
const { x, y } = hovered;
const enemy = core.status.thisMap.enemy.get(x, y);
if (enemy) mainUi.open('fixedDetail', { panel: 'critical' });
}
})
.realize('danmaku', () => {
openDanmakuPoster();
})
.realize('restart', () => {
core.confirmRestart();
})

View File

@ -17,7 +17,8 @@ export function patchAudio() {
};
patch.add('playBgm', function (bgm, startTime) {
play(bgm, startTime);
const name = core.getMappedName(bgm) as BgmIds;
play(name, startTime);
});
patch.add('pauseBgm', function () {
pause();

View File

@ -13,12 +13,13 @@ import { createWeather } from './weather';
export function createGameRenderer() {
const App = defineComponent(_props => {
return () => (
<container width={MAIN_WIDTH} height={MAIN_HEIGHT}>
<container noanti width={MAIN_WIDTH} height={MAIN_HEIGHT}>
{sceneController.render()}
</container>
);
});
mainRenderer.setAntiAliasing(false);
mainRenderer.hide();
createApp(App).mount(mainRenderer);

View File

@ -1,140 +0,0 @@
import { logger } from '@motajs/common';
import { MotaOffscreenCanvas2D } from '@motajs/render';
import { mainSetting } from '@motajs/legacy-ui';
import { Sprite, Transform } from '@motajs/render';
import { gameListener, hook } from '@user/data-base';
import {
ILayerGroupRenderExtends,
LayerGroup,
LayerGroupFloorBinder
} from '../elements';
export class LayerGroupHalo implements ILayerGroupRenderExtends {
id: string = 'halo';
group!: LayerGroup;
binder!: LayerGroupFloorBinder;
halo!: Halo;
static sprites: Set<Halo> = new Set();
awake(group: LayerGroup): void {
this.group = group;
const ex = group.getExtends('floor-binder');
if (ex instanceof LayerGroupFloorBinder) {
this.binder = ex;
this.halo = new Halo();
this.halo.setHD(true);
this.halo.size(group.width, group.height);
this.halo.setZIndex(75);
this.halo.binder = ex;
group.appendChild(this.halo);
LayerGroupHalo.sprites.add(this.halo);
} else {
logger.error(1401);
group.removeExtends('halo');
}
}
onDestroy(_group: LayerGroup): void {
this.halo?.destroy();
LayerGroupHalo.sprites.delete(this.halo);
}
}
const haloColor: Record<number, string[]> = {
21: ['cyan'],
25: ['purple'],
26: ['blue'],
27: ['red'],
31: ['#3CFF49'],
29: ['#51E9FF'],
32: ['#fff966']
};
class Halo extends Sprite {
/** 单元格大小 */
cellSize: number = 32;
/** 当前楼层,用于获取有哪些光环 */
binder!: LayerGroupFloorBinder;
constructor() {
super('static', true);
this.setRenderFn((canvas, transform) => {
this.drawHalo(canvas, transform);
});
}
drawHalo(canvas: MotaOffscreenCanvas2D, _transform: Transform) {
if (!mainSetting.getValue('screen.halo', true)) return;
const floorId = this.binder.getFloor();
if (!floorId) return;
const col = core.status.maps[floorId].enemy;
if (!col) return;
const [dx, dy] = col.translation;
const list = col.haloList.concat(
Object.keys(flags[`melt_${floorId}`] ?? {}).map(v => {
const [x, y] = v.split(',').map(v => parseInt(v));
return {
type: 'square',
data: {
x: x + dx,
y: y + dy,
d: 3
},
special: 25
};
})
);
const { ctx } = canvas;
const cell = this.cellSize;
ctx.lineWidth = 1;
for (const halo of list) {
if (halo.type === 'square') {
const { x, y, d } = halo.data;
let [color, border] = haloColor[halo.special];
let alpha = 0.1;
let borderAlpha = 0.6;
const { mouseX, mouseY } = gameListener;
if (mouseX === halo.from?.x && mouseY === halo.from?.y) {
alpha = 0.3;
borderAlpha = 0.8;
color = '#ff0';
border = '#ff0';
}
const r = Math.floor(d / 2);
const left = x - r;
const top = y - r;
ctx.fillStyle = color;
ctx.strokeStyle = border ?? color;
ctx.globalAlpha = alpha;
ctx.fillRect(left * cell, top * cell, d * cell, d * cell);
ctx.globalAlpha = borderAlpha;
ctx.strokeRect(left * cell, top * cell, d * cell, d * cell);
}
}
}
}
function updateHalo(block: Block) {
if (block.event.cls === 'enemys' || block.event.cls === 'enemy48') {
LayerGroupHalo.sprites.forEach(v => {
const floor = v.binder.getFloor();
if (floor === core.status.floorId) {
v.update();
}
});
}
}
hook.on('enemyExtract', col => {
LayerGroupHalo.sprites.forEach(v => {
const floor = v.binder.getFloor();
if (col.floorId === floor) {
v.update();
}
});
});
gameListener.on('hoverBlock', updateHalo);
gameListener.on('leaveBlock', updateHalo);

View File

@ -38,7 +38,6 @@ import { ReplayingStatus } from './toolbar';
import { getHeroStatusOn } from '@user/data-state';
import { hook } from '@user/data-base';
import { FloorDamageExtends, FloorItemDetail } from '../elements';
import { LayerGroupHalo } from '../legacy/halo';
import { FloorChange } from '../legacy/fallback';
import { mainUIController } from './controller';
import {
@ -57,7 +56,6 @@ const MainScene = defineComponent(() => {
const layerGroupExtends: ILayerGroupRenderExtends[] = [
new FloorDamageExtends(),
new FloorItemDetail(),
new LayerGroupHalo(),
new LayerGroupAnimate(),
new FloorViewport()
];
@ -252,7 +250,12 @@ const MainScene = defineComponent(() => {
};
return () => (
<container id="main-scene" width={MAIN_WIDTH} height={MAIN_HEIGHT}>
<container
id="main-scene"
width={MAIN_WIDTH}
height={MAIN_HEIGHT}
noanti
>
<LeftStatusBar
loc={[0, 0, STATUS_BAR_WIDTH, STATUS_BAR_HEIGHT]}
status={leftStatus}
@ -269,6 +272,7 @@ const MainScene = defineComponent(() => {
onClick={clickMap}
onDown={downMap}
onMove={moveMap}
noanti
>
<layer-group id="layer-main" ex={layerGroupExtends} ref={map}>
<layer layer="bg" zIndex={10}></layer>

View File

@ -127,7 +127,7 @@ export const MainSettings = defineComponent<MainSettingsProps>(props => {
choices={choices}
width={POP_BOX_WIDTH}
onChoose={choose}
maxHeight={MAIN_HEIGHT - 64}
maxHeight={MAIN_HEIGHT - 32}
interval={8}
scope={scope}
/>

View File

@ -31,7 +31,6 @@ import {
LayerGroup,
LayerGroupFloorBinder
} from '../elements';
import { LayerGroupHalo } from '../legacy/halo';
import { Font } from '@motajs/render-style';
import { clamp, mean } from 'lodash-es';
import { calculateStatisticsOne, StatisticsDataOneFloor } from './statistics';
@ -65,7 +64,6 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
const layerGroupExtends: ILayerGroupRenderExtends[] = [
new FloorDamageExtends(),
new FloorItemDetail(),
new LayerGroupHalo(),
new LayerGroupAnimate()
];

View File

@ -2,5 +2,4 @@ export { default as Box } from './box.vue';
export { default as BoxAnimate } from './boxAnimate.vue';
export { default as Column } from './colomn.vue';
export { default as EnemyOne } from './enemyOne.vue';
export { default as Minimap } from './minimap.vue';
export { default as Scroll } from './scroll.vue';

View File

@ -1,176 +0,0 @@
<template>
<canvas :id="`minimap-${id}`"></canvas>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue';
import { MinimapDrawer, getArea } from '../tools/fly';
import { useDrag, useWheel, requireUniqueSymbol } from '../use';
import { debounce } from 'lodash-es';
import { mainSetting } from '../preset/settingIns';
const props = defineProps<{
action?: boolean;
scale?: number;
noBorder?: boolean;
showInfo?: boolean;
autoLocate?: boolean;
width?: number;
height?: number;
}>();
const area = getArea();
const id = requireUniqueSymbol().toFixed(0);
const setting = mainSetting.getSetting('ui.mapLazy')!;
let canvas: HTMLCanvasElement;
let drawer: MinimapDrawer;
function onChange(floor: FloorIds) {
drawer.nowFloor = floor;
drawer.nowArea =
Object.keys(area).find(v => area[v].includes(core.status.floorId)) ??
'';
drawer.drawedThumbnail = {};
if (props.autoLocate) {
drawer.locateMap(drawer.nowFloor);
}
drawer.drawMap();
}
let scale = props.scale ?? 3;
let lastX = 0;
let lastY = 0;
let moved = false;
let startX = 0;
let startY = 0;
let touchScale = false;
let lastDis = 0;
let lastScale = scale;
const changeScale = debounce((s: number) => {
canvas.style.transform = '';
drawer.drawedThumbnail = {};
drawer.scale = s;
drawer.drawMap();
lastScale = s;
}, 200);
function resize(delta: number) {
drawer.ox *= delta;
drawer.oy *= delta;
scale = delta * scale;
changeScale(scale);
canvas.style.transform = `scale(${scale / lastScale})`;
drawer.thumbnailLoc = {};
}
function drag(x: number, y: number) {
if (touchScale) return;
const dx = x - lastX;
const dy = y - lastY;
drawer.ox += dx;
drawer.oy += dy;
lastX = x;
lastY = y;
drawer.checkMoveThumbnail();
drawer.drawToTarget();
if (Math.abs(x - startX) > 10 || Math.abs(y - startY) > 10) moved = true;
}
function touchdown(e: TouchEvent) {
if (e.touches.length >= 2) {
touchScale = true;
lastDis = Math.sqrt(
(e.touches[0].clientX - e.touches[1].clientX) ** 2 +
(e.touches[0].clientY - e.touches[1].clientY) ** 2
);
}
}
function touchup(e: TouchEvent) {
if (e.touches.length < 2) touchScale = false;
}
function touchmove(e: TouchEvent) {
if (!touchScale) return;
const dis = Math.sqrt(
(e.touches[0].clientX - e.touches[1].clientX) ** 2 +
(e.touches[0].clientY - e.touches[1].clientY) ** 2
);
resize(dis / lastDis);
lastDis = dis;
}
function afterBattle() {
if (!setting.value) {
requestAnimationFrame(() => {
drawer.drawedThumbnail = {};
drawer.drawMap();
});
}
}
onMounted(() => {
const width = props.width ?? 300;
const height = props.height ?? 300;
canvas = document.getElementById(`minimap-${id}`) as HTMLCanvasElement;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
canvas.width = width * devicePixelRatio;
canvas.height = height * devicePixelRatio;
drawer = new MinimapDrawer(canvas);
drawer.scale = props.scale ?? 3;
drawer.noBorder = props.noBorder ?? false;
drawer.showInfo = props.showInfo ?? false;
if (props.autoLocate) {
drawer.locateMap(drawer.nowFloor);
}
drawer.drawMap();
const { hook } = Mota.require('@user/data-base');
hook.on('afterChangeFloor', onChange);
hook.on('afterBattle', afterBattle);
if (props.action) {
useDrag(
canvas,
drag,
(x, y) => {
lastX = x;
lastY = y;
startX = x;
startY = y;
},
() => {
setTimeout(() => {
moved = false;
}, 50);
},
true
);
useWheel(canvas, (x, y) => {
const delta = -Math.sign(y) * 0.1 + 1;
resize(delta);
});
canvas.addEventListener('touchstart', touchdown);
canvas.addEventListener('touchend', touchup);
canvas.addEventListener('touchmove', touchmove);
}
});
onUnmounted(() => {
const { hook } = Mota.require('@user/data-base');
hook.off('afterChangeFloor', onChange);
hook.off('afterBattle', afterBattle);
});
</script>
<style lang="less" scoped></style>

View File

@ -1,488 +0,0 @@
import { EventEmitter } from 'eventemitter3';
import { logger } from '@motajs/common';
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>>;
interface DanmakuResponse extends ResponseBase {
total: number;
list: DanmakuInfo[];
}
interface DanmakuInfo {
id: string;
comment: string;
tags: string;
love: string;
my_love_type: boolean;
userid: string;
deler: string;
upload_time: string;
tower_name: string;
}
interface DanmakuPostInfo extends Partial<DanmakuContentInfo> {
type: 1 | 2 | 3;
towername: 'HumanBreak';
id?: number;
}
interface DanmakuContentInfo {
comment: string;
tags: string;
}
interface PostDanmakuResponse extends ResponseBase {
id: number;
}
interface PostLikeResponse extends ResponseBase {
liked: boolean;
}
interface DanmakuEvent {
showStart: [danmaku: Danmaku];
showEnd: [danmaku: Danmaku];
like: [liked: boolean, danmaku: Danmaku];
}
type SpecContentFn = (content: string, type: string) => VNode;
interface AllowedCSS {
property: string;
check: (value: string, prop: string) => true | string;
}
const allowedCSS: Partial<Record<CanParseCss, AllowedCSS>> = {
color: {
property: 'color',
check: () => true
},
backgroundColor: {
property: 'backgroundColor',
check: () => true
},
fontSize: {
property: 'fontSize',
check: value => {
if (!/^\d+%$/.test(value)) {
return '字体大小只能设置为百分格式';
}
if (parseInt(value) > 200) {
return '字体最大只能为200%';
}
return true;
}
}
};
export class Danmaku extends EventEmitter<DanmakuEvent> {
static backend: string = `/backend/tower/barrage.php`;
static all: Set<Danmaku> = new Set();
static allInPos: Partial<Record<FloorIds, Record<LocString, Danmaku[]>>> =
{};
static showList: Danmaku[] = shallowReactive([]);
static showMap: Map<number, Danmaku> = new Map();
static specList: Record<string, SpecContentFn> = {};
static lastEditoredDanmaku?: Danmaku;
id: number = -1;
text: string = '';
x: number = 0;
y: number = 0;
floor?: FloorIds;
showing: boolean = false;
likedNum: number = 0;
liked: boolean = false;
style: CSSObj = {};
textColor: string = 'white';
strokeColor: string = 'black';
private posted: boolean = false;
vNode?: VNode;
private posting: boolean = false;
/**
*
* @returns Axios Post Promise
*/
async post(): Promise<AxiosResponse<PostDanmakuResponse>> {
if (this.posted || this.posting) {
logger.warn(5);
return Promise.reject();
}
const data: DanmakuPostInfo = {
type: 2,
towername: 'HumanBreak',
...this.encode()
};
this.posting = true;
const form = toFormData(data);
// /* @__PURE__ */ form.append('userid', id);
// /* @__PURE__ */ form.append('password', password);
try {
const res = await axios.post<PostDanmakuResponse>(
Danmaku.backend,
form
);
this.id = res.data.id;
this.posting = false;
if (res.data.code === 0) {
this.posted = true;
tip('success', '发送成功');
this.addToList();
} else {
tip('error', res.data.message);
}
return res;
} catch (e) {
this.posted = false;
this.posting = false;
logger.error(1, String(e));
return Promise.reject();
}
}
/**
*
*/
encode(): DanmakuContentInfo {
const css = this.getEncodedCSS();
return {
comment: this.text,
tags: JSON.stringify([
`!css:${JSON.stringify(css)}`,
`!pos:${this.x},${this.y},${this.floor}`
])
};
}
/**
*
* @param info
*/
decode(info: DanmakuContentInfo) {
this.text = info.comment;
ensureArray(JSON.parse(info.tags) as string[]).forEach(v => {
if (v.startsWith('!css:')) {
this.style = JSON.parse(v.slice(5));
} else if (v.startsWith('!pos:')) {
const [x, y, f] = v.slice(5).split(',');
this.x = parseInt(x);
this.y = parseInt(y);
this.floor = f as FloorIds;
} else {
logger.warn(3, v);
}
});
}
getEncodedCSS() {
const css = JSON.parse(JSON.stringify(this.style)) as CSSObj;
if (!css.color) css.color = this.textColor;
if (!css.textShadow)
css.textShadow = `1px 1px 1px ${this.strokeColor}, 1px -1px 1px ${this.strokeColor}, -1px 1px 1px ${this.strokeColor}, -1px -1px 1px ${this.strokeColor}`;
return { ...css, ...this.style };
}
/**
*
* @param fill
* @param stroke
*/
color(fill?: string, stroke?: string) {
if (fill) this.textColor = fill;
if (stroke) this.strokeColor = stroke;
}
/**
*
* @param icon id
*/
addIcon(icon: AllIds) {
this.text += `[i:${icon}]`;
}
/**
* css信息
* @param str css字符串
* @param overwrite css
*/
css(str: string, overwrite?: boolean): void;
/**
* css信息
* @param str css对象 CSSStyleDeclaration
* @param overwrite css
*/
css(obj: CSSObj, overwrite?: boolean): void;
css(obj: string | CSSObj, overwrite: boolean = false) {
const res = typeof obj === 'string' ? parseCss(obj) : obj;
const allow = Danmaku.checkCSSAllow(res);
if (allow.length === 0) {
if (overwrite) this.style = res;
else {
this.style = { ...this.style, ...res };
}
} else {
logger.error(8, allow.join(','));
}
}
/**
*
*/
addToList() {
Danmaku.all.add(this);
if (!this.floor) return;
Danmaku.allInPos[this.floor] ??= {};
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`] ??= [];
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`].push(this);
}
/**
* VNode
*/
parse() {
let pointer = -1;
let ignore = false;
let str = '';
let spec = false;
let specType = '';
let specTypeEnd = false;
let specContent = '';
const children: VNode[] = [];
while (++pointer < this.text.length) {
const char = this.text[pointer];
if (char === '\\' && !ignore) {
ignore = true;
continue;
}
if (ignore) {
str += char;
continue;
}
if (char === '[') {
spec = true;
children.push(h('span', str));
str = '';
continue;
}
if (char === ']') {
if (!spec) {
logger.warn(4);
str += char;
} else {
spec = false;
specTypeEnd = false;
children.push(this.createSpecVNode(specType, specContent));
specType = '';
specContent = '';
}
continue;
}
if (spec) {
if (!specTypeEnd) {
if (char !== ':') {
specType += char;
} else {
specTypeEnd = true;
}
} else {
specContent += char;
}
continue;
}
str += char;
}
if (str.length > 0) {
children.push(h('span', str));
}
return h(
'span',
{ class: 'danmaku', style: this.getEncodedCSS() },
children
);
}
/**
* VNode
*/
getVNode(nocache: boolean = false) {
if (nocache) return (this.vNode = this.parse());
return this.vNode ?? (this.vNode = this.parse());
}
/**
*
*/
show() {
if (this.showing) return;
this.showing = true;
Danmaku.showList.push(this);
Danmaku.showMap.set(this.id, this);
this.emit('showStart', this);
}
/**
*
*/
showEnd() {
if (!this.showing) return;
this.showing = false;
deleteWith(Danmaku.showList, this);
Danmaku.showMap.delete(this.id);
this.emit('showEnd', this);
}
/**
*
*/
async triggerLike() {
const post: DanmakuPostInfo = {
type: 3,
towername: 'HumanBreak',
id: this.id
};
const form = toFormData(post);
// /* @__PURE__ */ form.append('userid', id);
// /* @__PURE__ */ form.append('password', password);
const res = await axios.post<PostLikeResponse>(Danmaku.backend, form);
if (res.data.code !== 0) {
logger.warn(18, this.id.toString());
tip('error', `Error ${res.data.code}. ${res.data.message}`);
} else {
tip('success', res.data.message);
if (res.data.liked) {
this.liked = true;
this.likedNum++;
} else {
this.liked = false;
this.likedNum--;
}
this.emit('like', this.liked, this);
}
return res;
}
/**
*
*/
destroy() {
this.showEnd();
Danmaku.all.delete(this);
if (this.floor) {
const floor = Danmaku.allInPos[this.floor];
if (floor) {
delete floor[`${this.x},${this.y}`];
}
}
}
private createSpecVNode(type: string, content: string): VNode {
if (Danmaku.specList[type]) {
return Danmaku.specList[type](content, type);
} else {
logger.warn(7, type);
}
return h('span');
}
/**
* CSS内容是否符合发弹幕要求
* @param css CSS内容
*/
static checkCSSAllow(css: CSSObj) {
const problem: string[] = [];
for (const [key, value] of Object.entries(css)) {
if (!allowedCSS[key as CanParseCss]) {
problem.push(`不允许的CSS:${key}`);
continue;
} else {
const res = allowedCSS[key as CanParseCss]!.check(value, key);
if (res !== true) {
problem.push(res);
}
}
}
return problem;
}
/**
*
*/
static async fetch() {
Danmaku.all.clear();
Danmaku.allInPos = {};
const form = toFormData({
type: 1,
towername: 'HumanBreak'
});
// /* @__PURE__ */ form.append('userid', id);
// /* @__PURE__ */ form.append('password', password);
const data = await axios.post<DanmakuResponse>(Danmaku.backend, form);
data.data.list.forEach(v => {
const dan = new Danmaku();
dan.id = parseInt(v.id);
dan.likedNum = parseInt(v.love);
dan.liked = v.my_love_type;
dan.decode(v);
dan.posted = true;
dan.addToList();
});
}
/**
*
* @param dan
*/
static show(dan: Danmaku) {
dan.show();
}
/**
*
* @param type
* @param fn VNode
*/
static registerSpecContent(type: string, fn: SpecContentFn) {
if (this.specList[type]) {
logger.warn(6, type);
}
this.specList[type] = fn;
}
}

View File

@ -1,72 +0,0 @@
{
"normal": [
{
"name": "虚惊一场",
"text": ["打完山洞门口的兽人后只剩一滴血"],
"point": 30
},
{
"name": "真能刷",
"text": [
"勇气之路的刷血怪刷到 <span style=\"color: gold\">15w</span> 以上的血"
],
"point": 30
}
],
"challenge": [
{
"name": "逃出生天",
"text": ["通过山路追逐战的困难难度"],
"point": 20
},
{
"name": "冰与火之舞",
"text": ["完成第二章音游特殊战的困难难度"],
"point": 50
}
],
"explore": [
{
"name": "勇气巅峰",
"text": ["第一章完成度达到100%"],
"progress": "${Mota.require('completion_r').getChapterCompletion(1)} / 100",
"percent": true,
"point": 50
},
{
"name": "你是怎么办到的?!",
"text": ["与山路上的若干个神秘木牌对话"],
"progress": "${core.getLocalStorage('mountSign', 0)} / 5",
"hide": "该探索成就需要你自己探索如何达成",
"point": 25
},
{
"name": "智慧之心",
"text": ["第二章完成度达到100%"],
"progress": "${Mota.require('completion_r').getChapterCompletion(2)} / 100",
"percent": true,
"point": 50
},
{
"name": "源头?",
"text": [
"在冰封雪原第一个山洞的水源处使用跳跃技能,并向前一步触发剧情"
],
"hide": "该探索成就需要你自己探索如何达成",
"point": 30
},
{
"name": "学坏了",
"text": ["学习电摇嘲讽技能"],
"hide": "该探索成就需要你自己探索如何达成",
"point": 20
},
{
"name": "满腹经纶",
"text": ["把第二章中所有能学习的技能都学一遍"],
"hide": "该探索成就需要你自己探索如何达成",
"progress": "",
"point": 50
}
]
}

View File

@ -1,523 +0,0 @@
{
"tip": {
"text": "注意事项",
"condition": "true",
"desc": [
"这里显示本塔中需要注意的事项。",
"<br>",
"<br>",
"1. <span style=\"color: yellow; font-weight: 700\">",
"本百科全书字数很多,可以选择性地阅读。</span>不过本条目最好可以全部阅读一遍。",
"<br>",
"<br>",
"2. 本百科全书的内容会<span style=\"color: gold\">随着游戏的推进而增加新内容</span>",
"同时每次增加新内容时都会有提示。",
"<br>",
"<br>",
"3. <span style=\"color: gold\">背包中的系统设置同样非常重要,有些问题可以在那里找到原因</span>。",
"例如当你获得技能时可能会发现开启不了技能,",
"就是因为你打开了<span style=\"color: gold\">自动切换技能</span>的功能,在系统设置里面有说。",
"<br>",
"<br>",
"4. <span style=\"color: yellow; font-weight: 700\">重要!!!</span>本塔没有考虑录像的二次播放性,",
"这意味着如果你从头播放一个录像,播放完成后继续游玩,提交成绩后不能保证绿录像,请谨慎考虑。",
"<br>",
"<br>",
"5. 本塔中<span style=\"color: gold\">几乎所有 ui 都可以纵向滚动</span>,如果发现显示不全,",
"可以尝试上下拖动,就像浏览网页一样。电脑端还可以使用滚轮上下滚动。",
"大部分可以纵向滚动的 ui 都会在右方有一个滚动条,也可以拖动它进行滚动,例如本百科全书的条目列表和",
"条目说明都是可以通过上述方式滚动的。",
"<br>",
"<br>",
"6. 本塔主要面向电脑端设计,",
"<span style=\"color: gold\">建议使用电脑游玩以获得更好的游戏体验同时使用约16:9的比例游玩更加合适",
"</span>。但是手机依然可以游玩本塔,",
"但部分操作可能不是很方便ui 也可能不是很美观,不过依然可以完整体验本游戏。",
"<br>",
"<br>",
"7. 对于手机端,可以点击<span style=\"color: gold\">右下角的难度文字</span>来切换工具栏至数字键。",
"这样,你可以更加方便地进行使用技能等操作。",
"<br>",
"<br>",
"8. 本塔中几乎所有 ui 在打开时都会有一个0.6s的动画,如果不想要,可以在开头捡的系统设置里面关闭(默认关闭)。",
"同时,几乎所有 ui 的退出按钮都在左上角。",
"<br>",
"<br>",
"9. 地图上显示的怪物临界有可能不准,当其与折线图有差异时,<span style=\"color: gold\">请以折线图为准</span>。"
]
},
"about": {
"text": "关于游戏",
"condition": "true",
"desc": [
"使用样板Vite 魔塔样板",
"<br>",
"样板版本V2.10.0",
"<br>",
"游戏版本V1.0.0-alpha",
"<br>",
"游戏作者:古祠",
"<br>",
"游戏开源地址:<a href=\"https://github.com/unanmed/HumanBreak\" target=\"_blank\">",
"https://github.com/unanmed/HumanBreak</a>",
"<br>",
"本塔遵循MIT开源协议。<a href=\"LICENSE\" target=\"_blank\">查看开源协议</a>",
"<br>",
"音乐来源:网易云音乐等",
"<br>",
"素材来源:大素材库、爱给网、网站素材库等",
"<br>",
"<span style=\"color: gold\">特别说明:素材与音乐均来自网络,不得用于商业用途,仅用于参考与学习</span>",
"<br>",
"特别鸣谢(排名不分先后):",
"<br>",
"1. 无名甲烷菌(提供部分特殊属性与机制想法)",
"<br>",
"测试(排名不分先后):",
"<br>",
"1. 永葆一颗童心",
"<br>",
"2. 影法师",
"<br>",
"3. 夜战天明889",
"<br>",
"4. 霸道的老鼠"
]
},
"tutorial": {
"text": "新手教程",
"condition": "true",
"desc": [
"本条目是魔塔游戏的新手教程,如果对魔塔有一定的了解,可以直接忽略。",
"<br>",
"<br>",
"魔塔是一种固定数值rpg游戏在打怪的时候遵循<span style=\"color: gold\">我打你一下,你打我一下</span>",
"的原则,造成的伤害是己方攻击减去对方防御,最后怪物的伤害便是你在战斗中失去的生命值。当然,为了游戏体验,",
"战斗过程会被省略。",
"<br>",
"<br>",
"宝石可以增加你的属性,在大部分魔塔中,红宝石增加攻击,蓝宝石增加防御,本塔也不例外。血瓶可以增加你的生命值。",
"一般情况下,拾取宝物的优先级是<span style=\"color: gold\">红宝石 &gt; 蓝宝石 &gt; 血瓶</span>",
"但部分情况可能不是这样,这需要你自己的游玩经验等。",
"<br>",
"<br>",
"本塔还拥有升级机制,升级时能够给你增加大量的属性,因此,一般情况下当你接近升级时,需要尽快打怪升级。",
"<br>",
"<br>",
"然后是门。在魔塔中,很多门都不是必开的门,它们的作用一般是可以躲开怪物拿宝石,或者门里面有血瓶等。",
"当你血量足够时,这些门可以不用开,不然可能会有必开的门无法开启导致卡关。对于钥匙,每种颜色的钥匙开对应颜色的门,",
"价值是<span style=\"color: gold\">红 &gt; 蓝 &gt; 黄</span>。",
"<br>",
"<br>",
"为了更加方便,本塔增加了宝石血瓶显示数据的功能,这样你可以清晰地知道每个宝石增加了多少属性。",
"<br>",
"<br>",
"下面是勇士基础属性的说明:",
"<br>",
"<span style=\"color: lightgreen\">1. 生命值</span>",
"勇士的血量,当它归零时,游戏结束",
"<br>",
"<span style=\"color: lightcoral\">2. 攻击</span>",
"勇士的攻击,攻击越高,每回合对怪物造成的伤害越高",
"<br>",
"<span style=\"color: lightblue\">3. 防御</span>",
"勇士的防御,防御越高,怪物每回合对你造成的伤害越低",
"<br>",
"<span style=\"color: green\">4. 经验</span>",
"勇士的经验,到达一定值后会升级。本塔在状态栏中显示为距离升级剩余的经验",
"<br>",
"<span style=\"color: gold\">5. 金币</span>",
"勇士的金币,可以用于购买物品。本塔中在进入第二章后会有用",
"<br>",
"<span style=\"color: lightgreen\">6. 护盾</span>",
"勇士的护盾,用处是能够在战后减少同等数值的伤害,在本塔中可以使伤害变为负值。本塔中,在点开无上之盾技能后,",
"智慧会充当护盾。更多信息可以查看“勇士属性”条目。"
]
},
"noun": {
"text": "名词解释",
"condition": "true",
"desc": [
"本条目会解释诸如临界等魔塔术语,对魔塔有一定了解的可以直接忽略。",
"<br>",
"<br>",
"<span style=\"color: lightcoral\">1. 临界</span>",
"在魔塔中,临界是一个非常重要的东西。首先,我们很容易可以得到,吃攻击时只有当减少了战斗回合数时怪物的伤害会减少,",
"那么,吃攻击时怪物的减伤是不连续的。而<span style=\"color: gold\">距离下一次减少怪物的伤害需要加的攻击的量</span>",
"便是临界。当我们吃一个攻击恰好使怪物伤害减少时,称为“踩临界”。一般情况下,踩临界的减伤要比吃防御要高,",
"因此,当能踩到临界时,我们应当先踩临界,再吃防御。",
"<br>",
"<br>",
"<span style=\"color: lightblue\">2. 加防</span>",
"加防指的是加防对怪物的减伤。在本塔中会以“n防”的形式显示在怪物手册或其他地方。在本塔中一般你不需要刻意计算",
"临界与加防减伤,你可以在怪物手册中<span style=\"color: gold\">查看减伤折线图</span>",
"更多信息请查看“怪物手册”条目。",
"<br>",
"<br>",
"<span style=\"color: gold\">3. 咸鱼</span>",
"一般来讲,开不必开的门,或者使用不必使用的道具被称为咸鱼,或者是咸门,咸道具。一般情况下,说“咸”便是指咸鱼。",
"一般情况下,门后面有宝石且无法通过其他方式进入的都是必开门,而只有血瓶的都是咸鱼门。"
]
},
"shortcut": {
"text": "快捷键",
"condition": "true",
"desc": [
"这里包含本塔中所有的快捷键。对于手机端,可以点击工具栏的难度的位置切换工具栏至数字键。",
"下面会分为样板快捷键和本塔快捷键两类分别说明。可以ctrl+F进行搜索快捷键的功能。",
"<br>",
"<br>",
"下面是样板中的所有快捷键:",
"<br>",
"<span style=\"color: gold\">X</span>:打开怪物手册",
"<br>",
"<span style=\"color: gold\">S</span>:打开存档界面",
"<br>",
"<span style=\"color: gold\">D</span>:打开读档界面",
"<br>",
"<span style=\"color: gold\">A或5</span>:读取自动存档",
"<br>",
"<span style=\"color: gold\">W或6</span>:撤销读取的自动存档",
"<br>",
"<span style=\"color: gold\">Q</span>:打开装备栏",
"<br>",
"<span style=\"color: gold\">T</span>:打开道具栏",
"<br>",
"<span style=\"color: gold\">G</span>:打开楼层传送器",
"<br>",
"<span style=\"color: gold\">Z或单击勇士</span>:勇士转向",
"<br>",
"<span style=\"color: gold\">空格或双击勇士或7</span>:轻按(拾取勇士周围的宝物但不移动勇士)",
"<br>",
"<span style=\"color: gold\">Esc</span>:打开游戏菜单",
"<br>",
"<span style=\"color: gold\">R</span>:打开录像回放菜单",
"<br>",
"<span style=\"color: gold\">N</span>:询问是否返回游戏主菜单",
"<br>",
"<span style=\"color: gold\">V</span>:打开快捷商店",
"<br>",
"<span style=\"color: gold\">B</span>:打开数据统计界面",
"<br>",
"<span style=\"color: gold\">Alt + 数字键</span>:快速换装",
"<br>",
"<span style=\"color: gold\">PgUp或PgDn</span>:浏览地图",
"<br>",
"<span style=\"color: gold\">P</span>:打开评论区",
"<br>",
"<br>",
"下面是本塔中新增的快捷键(不包括技能,技能快捷键请在查看技能界面中查看):",
"<br>",
"<span style=\"color: gold\">M</span>:快速标记怪物",
"<br>",
"<span style=\"color: gold\">J</span>:打开技能树",
"<br>",
"<span style=\"color: gold\">H</span>:打开百科全书",
"<br>",
"<span style=\"color: gold\">E</span>:查看鼠标位置怪物的特殊属性信息",
"<br>",
"<span style=\"color: gold\">C</span>:查看鼠标位置怪物的详细临界信息"
]
},
"extraAttr": {
"text": "勇士属性",
"condition": "true",
"desc": [
"这里只对本塔中新增的勇士属性进行说明。",
"<br>",
"<br>",
"<span style=\"color: lightblue\">1. 智慧</span>",
"智慧是该塔的核心属性之一。智慧可用于智慧加点,该功能会在进入第一章后开启。使用智慧可以点技能树。",
"除此之外,智慧也有其它功能。例如点开无上之盾技能后智慧还可以充当护盾,第二章点开学习技能后可以使用智慧学习怪物技能等。",
"<br>",
"<br>",
"<span style=\"color: lightgreen\">2. 生命回复</span>",
"生命回复指的是勇士每回合回复的生命值。当与怪物战斗时,勇士每回合都会回复对应量的生命值。因此,当吃攻击时,",
"与怪物战斗的回合数可能会减少,导致生命回复的总回复量减少。不过大部分情况下不需要在意这一点,",
"减少一回合并不会对吸的血造成很大的影响,除了一些特殊情况。",
"该项会显示在状态栏的生命值右方偏下的位置。该项不会超过勇士防御的十分之一,如果真实值溢出,那么多余部分会忽略,",
"当防御提高时,其值会一同改变",
"<br>",
"<br>",
"<span style=\"color: lightcoral\">3. 额外攻击</span>",
"额外攻击指的是勇士每回合的额外造成的伤害。一般情况下,当勇士破了怪物的防御时,该项便会起作用。",
"额外攻击相当于魔攻,无法通过一般方式减免。当勇士攻击怪物时,每回合都会附加对应量的伤害,对坚固怪同样有效。",
"额外攻击会显示在状态栏的攻击右方偏下的位置。"
]
},
"statusBar": {
"text": "状态栏",
"condition": "true",
"desc": [
"在本塔中,状态栏与游戏画面是分开的。<span style=\"color: gold\">你可以自由拖动状态栏,也可以修改其大小</span>。",
"具体方法如下:点击一下状态栏之后,左上角的拖拽图标会放大,此时你可以按住它拖动状态栏。",
"你可以直接将鼠标放到状态栏的边框上,然后直接拖动以改变状态栏的大小。手机端可以先点击一下状态栏使边框",
"变宽,然后拖动。电脑端点击状态栏也可以使边框变宽。如果你想折叠状态栏,完全可以拖动状态栏的下边框,",
"然后直接拖动至上方,这时状态栏便会变成一条线,相当于折叠了状态栏",
"<br>",
"<br>",
"<span style=\"color: gold\">状态栏可以纵向滚动</span>",
"如果你发现状态栏显示不全,可以尝试拉大状态栏,或者纵向拖动状态栏,就像网页上下滚动一样。",
"电脑端还可以使用滚轮上下滚动。",
"<br>",
"<br>",
"如果你觉得状态栏有些碍事,你完全可以将其缩小,或者把它放到不碍事的地方。",
"<br>",
"<br>",
"状态栏上面可能会有按钮,你可以直接点击。",
"<br>",
"<br>",
"对状态栏布局的说明。",
"<br>",
"本塔的状态栏的布局较为灵活。它是横向的布局,在状态栏较宽时可以看到,属性会横向依次显示。按照显示顺序,",
"状态栏显示项依次为:",
"<br>",
"<br>",
"1. <span style=\"color: gold\">楼层名</span>,点击后进入浏览地图界面",
"<br>",
"2. <span style=\"color: gold\">勇士等级</span>",
"<br>",
"3. <span style=\"color: gold\">当前开启的技能</span>",
"<br>",
"4. <span style=\"color: lightgreen\">当前勇士生命值</span>,右方偏下为每回合回复的生命值",
",当点开治愈之泉技能时,右方偏上会显示距离增加生命回复剩余血瓶数",
"<br>",
"5. <span style=\"color: lightcoral\">当前勇士的攻击</span>,右方偏下为勇士的额外攻击",
"<br>",
"6. <span style=\"color: lightblue\">当前勇士的防御</span>,当有魔法防御时,右方偏下为勇士的魔法防御",
"<br>",
"7. <span style=\"color: lightgreen\">当前勇士的智慧</span>,可以用于智慧加点等",
"<br>",
"8. <span style=\"color: gold\">当前勇士的金币</span>",
"<br>",
"9. <span style=\"color: lightgreen\">当前勇士距离升级剩余经验数</span>",
"<br>",
"10. <span style=\"color: gold\">三色钥匙</span>",
"<br>",
"11. <span style=\"color: gold\">打开技能树</span>(进入第一章后开启)",
"<br>",
"12. <span style=\"color: gold\">查看勇士的技能</span>(进入第一章后开启)"
]
},
"markEnemy": {
"text": "标记怪物",
"condition": "true",
"desc": [
"标记怪物可以使你能够更加方便地了解一个怪物的情况。",
"<br>",
"你可以通过以下两种方式标记怪物:",
"<br>",
"1. 打开怪物手册,选中怪物,进入怪物更多信息栏,点击标记怪物。",
"<br>",
"2. 将鼠标移动到你想要标记的怪物上面,<span style=\"color: gold\">",
"按下M键</span>,即可标记怪物,注意浏览地图中不能用该方式标记。",
"手机端暂时没有快速标记怪物的方式。",
"<br>",
"<br>",
"<span style=\"color: gold\">当一个怪物被标记后,怪物会有以下行为</span>",
"<br>",
"1. 当勇士恰好能打败怪物时,会进行提示",
"<br>",
"2. 当怪物的伤害恰好低于勇士生命值的2/3或1/3时会进行提示",
"<br>",
"3. 当勇士恰好踩到怪物的临界时,会进行提示",
"<br>",
"4. 当怪物零伤时,会进行提示",
"<br>",
"5. 被标记的怪物会出现类似于状态栏的盒子,可以随意拖动和改变大小。你也可以选择关闭这个盒子,",
"被关闭后可以通过重新标记来打开。这个盒子会显示标记的怪物的临界与伤害信息等,与状态栏一样,可以纵向滚动。",
"<br>",
"<br>",
"这个功能可以用于标记boss或者较强的挡路怪当这些怪能够攻击时你可以直接收到信息不需要再时刻费心注意怪物的伤害。",
"<br>",
"<br>",
"<span style=\"color: gold\">注意,标记的怪物是不计入存档的,同时标记的怪物只在本次游戏中有效,刷新页面后便会消失。</span>"
]
},
"book": {
"text": "怪物手册",
"condition": "true",
"desc": [
"本塔的怪物手册功能很多,下面一一介绍。",
"<br>",
"<br>",
"首先你可以按X打开怪物手册。除此之外将鼠标移动到怪物上也可以定点查看怪物的粗略信息。",
"将鼠标移动到一个怪物上,按下<span style=\"color: gold\">",
"E键</span>,可以查看该怪物的特殊属性信息。按下<span style=\"color: gold\">",
"C键</span>,可以查看该怪物的详细临界信息。",
"<br>",
"<br>",
"怪物手册打开的时候有一个0.6秒的动画,如果不想要可以在开头捡的系统设置里面关闭(默认关闭)。",
"<br>",
"<br>",
"打开怪物手册后,怪物手册的布局与样板自带的类似。与样板不同的是,这里的怪物手册不再是翻页式结构。",
"<span style=\"color: gold\">这里的怪物手册是滚动式结构</span>",
"你可以像浏览网页一样,用手指或鼠标上下滚动或者拖动右边的滚动条,电脑端还可以使用滚轮。",
"对于电脑端还可以使用键盘操作。上和下可以上下选择怪物左和右可以向上或向下移动5个怪物。这些操作与样板都类似。",
"<br>",
"<br>",
"点击一个怪物或者按下回车空格后,将进入怪物详细信息界面。这个界面分为多个栏,分别是特殊属性栏,详细临界栏,更多信息栏。",
"进入怪物详细信息后默认在特殊属性栏,该栏可以查看怪物的特殊属性。",
"注意特殊属性依然可以纵向滚动。在特殊属性下方,",
"是怪物的临界表,可以粗略地查看怪物的临界信息。在下方,你可以点击详细临界信息进入详细临界栏。",
"<br>",
"<br>",
"在详细临界栏中,怪物的伤害会以<span style=\"color: gold\">可视化折线图</span>的方式显示出来,",
"从而你可以更为清晰地看出怪物减伤趋势。",
"除了查看怪物伤害曲线,你还可以规划宝石。每个折线图下方都有一个滑动条,你可以拖动来模拟吃宝石。",
"注意,拖动时,滑动条左边会显示当前的加攻或加防次数,这个数值指的是在勇士所在地图中需要吃的最弱的宝石数量。",
"例如当前勇士所在地图中最弱的宝石加2点攻击加攻次数为3那么勇士的攻击增加量就为6。",
"勇士增加的攻击数值也会在下方显示。当加攻次数和加防次数改变时,折线图也会变化。",
"当前状态下怪物的伤害以及减伤总量也会在下方显示。<span style=\"color: gold\">",
"注意在此栏中无法通过点击屏幕回到怪物手册界面,更多信息请查看最后一段</span>。",
"<br>",
"<br>",
"在特殊属性栏,点击下方的怪物更多信息可以进入更多信息栏。此栏中,你可以查看怪物描述。但这不是这一栏的核心功能。",
"这一栏的核心功能是标记怪物。被标记的怪物会有一些非常方便的行为,这些行为可以在“",
"<span style=\"color: gold\">标记怪物</span>”条目中查看。",
"<br>",
"<br>",
"注意,在怪物详细信息中,除详细临界栏外均可以通过点击屏幕返回到怪物手册界面。",
"如果你是电脑端,在任意栏目中<span style=\"color: gold\">按下X键</span>会退出怪物手册,返回游戏,",
"<span style=\"color: gold\">按下回车Enter键</span>会回到怪物手册界面。"
]
},
"fly": {
"text": "楼层传送器",
"condition": "true",
"desc": [
"楼传界面打开时会有一个0.6秒的动画,如果不想要可以在开头捡的系统设置里面关闭。(默认关闭)",
"<br>",
"<br>",
"本塔的楼层传送器是一个集<span style=\"color: gold\">分区、小地图、楼层传送、浏览地图</span>于一体的多功能楼传。",
"<a href=\"https://unanmed.github.io/HumanBreak/maps/index.html\" target=\"_blank\">你也可以点击这里</a>查看所有区域的缩略图。",
"下面是楼传的具体说明:",
"<br>",
"<br>",
"首先,对于电脑端,最左侧显示区域信息,手机端则在上方的左侧。",
"<br>",
"<br>",
"然后,区域的右侧是小地图栏,这一栏会显示楼层的平面结构。你可以拖动,也可以使用滚轮或者双指放缩,当放缩到一定大小时,",
"会显示地图的缩略图。直接点击地图也可以选中地图,再次点击会传送至目标地图。",
"<br>",
"<br>",
"对于电脑端,最右侧是当前选中的地图的缩略图,手机则在下方,点击缩略图也可以传送。缩略图的下方是当前选中的地图名,",
"左右各有两个按钮表示后退10层、后退1层、前进1层、前进10层与样板的楼传的按钮功能类似对于小地图无法显示的单层",
"可以使用该功能到达。",
"<br>",
"<br>",
"最下方是设置按钮,可以切换无边框模式,电脑端还可以切换传统按键模式,传统按键模式下按键遵循样板的楼传按键方式。",
"对于非传统模式,<span style=\"color: gold\">上下左右</span>可以移动地图,",
"<span style=\"color: gold\">PageUp和PageDown</span>可以前进1层或后退1层。"
]
},
"tools": {
"text": "道具栏与装备栏",
"condition": "true",
"desc": [
"道具栏与装备栏打开时会有一个0.6秒的动画,如果不想要可以在开头捡的系统设置里面关闭。(默认关闭)",
"<br>",
"<br>",
"本塔的道具栏没有特别之处,这里不需要说明。主要是装备栏。",
"<br>",
"<br>",
"本塔的装备栏手机和电脑端不同,电脑端比手机端多了一个勇士属性的显示。在装备栏的装备列表栏,",
"<span style=\"color: gold\">上方有两个选择框与一个排序方式的选项</span>。",
"这三个可以筛选你拥有的装备并进行排序,从而让你能够更清楚地知道哪个装备更强。",
"第一个选择框可以筛选装备增加的属性,如果装备不增加选择的属性,那么会不显示。第二个选择框可以筛选增加的属性的方式,",
"有数值增加和百分比增加两种。在这个选择框右边有一个图标,这个图标可以改变武器的排序方式,有升序和降序两种,默认为升序。",
"例如你拥有两个装备分别增加10攻击和20攻击三者你分别选择了攻击数值升序那么增加10攻击的装备会排在上面",
"而增加20攻击的装备会排在下面。",
"<br>",
"<br>",
"对于电脑端,如果你想装装备,<span style=\"color: gold\">可以直接拖动装备至装备孔</span>",
"也可以选中装备后再次点击。<span style=\"color: gold\">手机端暂时无法拖动装备</span>。当选中一个装备后,",
"电脑端和手机端均会显示装备增加或减少的属性,注意有的装备可能<span style=\"color: gold\">不增加属性但是有特殊功能</span>。",
"对于电脑端,还会直接在勇士属性栏显示增加或减少的属性。"
]
},
"achievement": {
"text": "成就",
"condition": "true",
"desc": [
"成就系统是本塔的一个独立系统。它不会像勇士属性一样跟随存档变化,而是只要你完成了成就,那么就永远完成了,",
"除非你清理了浏览器。每个成就都有成就点,<span style=\"color: gold\">成就点目前没有实际用途,",
"只是一个收集要素,对游戏进程没有任何影响。</span>",
"<br>",
"<br>",
"成就分为三种,普通成就,挑战成就和探索成就。普通成就完成难度一般较低,挑战成就完成难度较高,",
"而探索成就一般需要你自己探索如何完成。对于完成度类型的探索成就,它的完成度由到达过的地图与本章完成的成就数决定。",
"<br>",
"<br>",
"<span style=\"color: gold\">调试模式下无法完成成就!</span>"
]
},
"score": {
"text": "计分方式",
"condition": "true",
"desc": [
"第一章计分方式:血量 + 黄 * 5000 + 蓝 * 15000",
"<br>",
"第二章计分方式:血量 / 10 + 黄 * 2000 + 蓝 * 5000 + 红 * 10000"
]
},
"skillTree": {
"text": "技能树",
"condition": "flags.chapter > 0",
"desc": [
"打开技能树可以点击状态栏的<span style=\"color: gold\">",
"技能树按钮</span>(如果发现没有显示可以尝试上下滚动状态栏),还可以按",
"<span style=\"color: gold\">快捷键J</span>打开。",
"<br>",
"<br>",
"技能树是本塔的主要玩法之一。它可以让你使用智慧来学习技能,增加属性等。智慧在状态栏显示在防御的下一项,",
"绿宝石可以增加勇士的智慧。",
"<br>",
"<br>",
"打开技能树页面后,你可以在上方看到技能的名称与描述,下方会显示技能树,以及升级要求等。点击一个技能可以选中技能,",
"再次点击可以升级技能。注意,前置技能栏可以上下滚动,因此如果发现显示不全,可以尝试上下滚动前置技能栏",
"<br>",
"<br>",
"注意,技能在点开之后是无法取消的,因此,加点时请慎重加点。注意,部分技能是必点技能,这些技能会在技能说明中明确指出,",
"这些技能一般需要尽早点出。"
]
},
"special1": {
"text": "第一章怪物特技",
"condition": "flags.chapter > 0",
"desc": [
"这里会展示第一章的怪物中需要特别说明的怪物特技。",
"<br>",
"<br>",
"<span style=\"color: #c0b088\">1. 坚固</span>",
"在本塔中,额外攻击可以对坚固怪造成额外伤害。",
"<br>",
"<br>",
"<span style=\"color: #80eed6\">2. 绝对防御</span>",
"该怪物一般可以用于刷血。该怪物可以使你每回合对怪物造成的伤害恰好为1导致战斗回合数很高因此可以刷血。",
"<br>",
"<br>",
"<span style=\"color: #fc3\">3. 致命一击、勇气之刃、勇气冲锋</span>",
"造成的伤害为怪物每回合对勇士的伤害的一定倍数,而非攻击提高一定倍数。"
]
},
"special2": {
"text": "第二章怪物特技",
"condition": "flags.chapter > 1",
"desc": [
"这里会展示第二章的怪物中需要特别说明的怪物特技。",
"<br>",
"<br>",
"<span style=\"color: #f66\">1. 电摇嘲讽</span>",
"该特技会撞碎路上的所有地形和门,不需要消耗钥匙,拾取路上的所有道具,与路上的怪物战斗,最后与该怪物战斗。",
"如果怪物所在位置可以被嘲讽,那么勇士会被继续嘲讽。如果在被嘲讽的路上可以被其他怪物嘲讽,则不会触发。",
"如果一个点可以被多个怪物嘲讽,那么会优先选择最靠左上角的怪物。在地图上会标记出勇士的移动方向。",
"<span style=\"color: gold\">在被嘲讽之前会自动存档。</span>",
"<br>",
"<br>",
"<span style=\"color: #d8a\">2. 永夜</span>、<span style=\"color: #ffd\">极昼</span>",
"战斗后会在本楼层中加减怪物与勇士的攻防每个楼层会单独存储。例如你在1楼层增加了100点攻击2楼层减少了100点攻击",
"那么当你从2楼层到1楼层时攻击会增加200点反之亦然。注意这里没有计算buff。"
]
}
}

View File

@ -1,43 +0,0 @@
{
"none": {
"text": "无",
"opened": "true",
"desc": [
"当前未选择技能"
]
},
"blade": {
"text": "1断灭之刃",
"opened": "true",
"desc": [
"<span style=\"color: gold\">快捷键1</span>,开启后勇士攻击增加${level:2 * 10}%",
"同时防御减少${level:2 * 10}%。",
"<br>",
"<br>",
"当前等级:${level:2}"
]
},
"jump": {
"text": "2跳跃",
"opened": "true",
"desc": [
"<span style=\"color: gold\">快捷键2</span>消耗200点生命值困难消耗400点一个地图只能使用3次",
"如果前方为可通行的地面,则不能使用该技能,如果前方为怪物,则将怪物移至勇士视线上第一个不能通行的方块后",
"如果前方为障碍物,则直接跳到该障碍物的后方。",
"<br>",
"<br>",
"进入第二章后不再消耗生命值。"
]
},
"shield": {
"text": "3铸剑为盾",
"opened": "true",
"desc": [
"<span style=\"color: gold\">快捷键3</span>,开启后勇士防御增加${level:10 * 10}%",
"同时攻击减少${level:10 * 10}%。",
"<br>",
"<br>",
"当前等级:${level:10}"
]
}
}

View File

@ -1,37 +0,0 @@
[
"按下C可以查看鼠标位置怪物临界",
"按下E可以查看鼠标位置怪物的详细属性",
"将鼠标移动到光环怪上以查看其产生的光环",
"字体太大?试试在背包的系统设置里面调整字体大小吧!",
"字体太小?试试在背包的系统设置里面调整字体大小吧!",
"按键不合心意?试试在背包的系统设置里面自定义快捷键",
"拖动状态栏左上角可以移动状态栏哦!",
"拖动状态栏右下角可以缩放状态栏哦!",
"按下M键鼠标位置的怪物的信息就会被你看光啦",
"咱就是说,要不要试一下工具栏的最后一个按钮?",
"要不要试试工具栏倒数第二个按钮呢?",
"想自定义工具栏?去背包的系统设置看看吧!",
"冷知识:临界界面可以拖动滚动条来查看减伤情况",
"可以用滚轮或者双指缩放小地图!",
"楼传的最左侧一栏可以选择区域!",
"冷知识:装备栏左栏最上面可以修改装备排序",
"冷冷冷知识:装备栏左栏最上面右侧可以更改顺序或倒序",
"第一章使用跳跃技能可是要扣血的!要注意!",
"按H查看本游戏的百科全书",
"给别人炫耀一下自己的成就点吧!虽然不能记榜(",
"抱团属性会在怪物右上角显示加成数量!",
"乾坤挪移属性会在怪物左上角显示“乾”字!",
"电脑端可以试试按F11全屏游玩",
"手机端要不试试横屏玩?",
"不在楼梯边也可以使用楼传!",
"技能树的右下角可以切换章节!",
"开启自动切换技能就会自动帮你选择最优技能了!",
"魔塔不仅有撤回还有恢复按W或6就可以了",
"觉得卡顿?可以去试着设置里面关闭一些特性!",
"从第二章开始怪物负伤害量不会超过其生命的1/4",
"生命回复不会超过防御的十分之一",
"不想看小贴士?设置里面可以关掉!",
"不小心进入了追猎范围?读取自动存档撤回到进入前吧!",
"不小心进入了电摇嘲讽范围?读取自动存档撤回到进入前吧!",
"小地图出现卡顿?试试在背包中系统设置里把小地图懒更新打开吧!"
]

View File

@ -1 +0,0 @@
export * from './webgl';

View File

@ -1,288 +0,0 @@
import { ensureArray } from '../utils';
import { sleep } from 'mutate-animate';
import { logger } from '@motajs/common';
import { tip } from '../use';
const { gl, gl2 } = checkSupport();
function checkSupport() {
const canvas = document.createElement('canvas');
const canvas2 = document.createElement('canvas');
const gl = canvas.getContext('webgl');
const gl2 = canvas2.getContext('webgl2');
if (!gl) {
sleep(3000).then(() => {
tip(
'warning',
`您的浏览器不支持WebGL大部分效果将会无法显示请更新你的浏览器`
);
});
}
if (!gl2) {
sleep(3000).then(() => {
tip(
'warning',
`您的浏览器不支持WebGL2大部分效果将会无法显示请更新你的浏览器`
);
});
}
return { gl: !!gl, gl2: !!gl2 };
}
export function isWebGLSupported() {
return gl;
}
export function isWebGL2Supported() {
return gl2;
}
export type WebGLColorArray = [number, number, number, number];
interface WebGLShaderInfo {
vertex: WebGLShader;
fragment: WebGLShader;
}
type UniformBinderNum = 1 | 2 | 3 | 4;
type UniformBinderType = 'f' | 'i';
type UniformFunc<
N extends UniformBinderNum,
T extends UniformBinderType,
V extends 'v' | ''
> = `uniform${N}${T}${V}`;
type UniformBinderValue<N extends UniformBinderNum> = N extends 1
? number
: N extends 2
? [number, number]
: N extends 3
? [number, number, number]
: [number, number, number, number];
interface UniformBinder<
N extends UniformBinderNum,
T extends UniformBinderType,
V extends 'v' | ''
> {
value: UniformBinderValue<N>;
set(value: UniformBinderValue<N>): void;
get(): UniformBinderValue<N>;
}
abstract class WebGLBase {
abstract canvas: HTMLCanvasElement;
abstract gl: WebGLRenderingContext | WebGL2RenderingContext;
background: WebGLColorArray = [0, 0, 0, 0];
vsSource: string = '';
fsSource: string = '';
program: WebGLProgram | null = null;
shader: WebGLShaderInfo | null = null;
resetCanvas() {
this.gl.clearColor(...this.background);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
}
setSize(width: number, height: number) {
this.canvas.width = width;
this.canvas.height = height;
}
compile() {
const gl = this.gl;
gl.deleteProgram(this.program);
gl.deleteShader(this.shader?.vertex ?? null);
gl.deleteShader(this.shader?.fragment ?? null);
this.program = this.createProgram();
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.useProgram(this.program);
}
vs(vs: string) {
this.vsSource = vs;
}
fs(fs: string) {
this.fsSource = fs;
}
/**
*
* @param uniform
* @param num float和int视为1vec2 vec3 vec4分别视为 2 3 4
* @param type 'f''i'
* @param vector 'v'''
* @returns uniform绑定器uniform
*/
createUniformBinder<
N extends UniformBinderNum,
T extends UniformBinderType,
V extends 'v' | ''
>(uniform: string, num: N, type: T, vector: V): UniformBinder<N, T, V> {
if (!this.program) {
throw new Error(
`Uniform binder should be use when the program initialized.`
);
}
const suffix = `${num}${type}${vector ? 'v' : ''}`;
const func = `uniform${suffix}` as UniformFunc<N, T, V>;
const value = (
num === 1 ? 0 : Array(num).fill(0)
) as UniformBinderValue<N>;
const loc = this.gl.getUniformLocation(this.program, uniform);
const gl = this.gl;
return {
value,
set(value) {
this.value = value;
let v;
if (vector === 'v') {
let _v = ensureArray(value);
if (type === 'f') {
v = new Float32Array(_v);
} else {
v = new Int32Array(_v);
}
} else {
v = ensureArray(value);
}
// 对uniform赋值
if (vector === 'v') {
// @ts-ignore
gl[func](loc, v);
} else {
// @ts-ignore
gl[func](loc, ...v);
}
},
get() {
return this.value;
}
};
}
protected createProgram() {
const gl = this.gl;
const vs = this.loadShader(gl.VERTEX_SHADER, this.vsSource);
const fs = this.loadShader(gl.FRAGMENT_SHADER, this.fsSource);
this.shader = {
vertex: vs,
fragment: fs
};
const program = gl.createProgram()!;
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
logger.error(9, gl.getProgramInfoLog(program) ?? '');
}
return program;
}
protected loadShader(type: number, source: string) {
const gl = this.gl;
const shader = gl.createShader(type)!;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(
`Cannot compile ${
type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'
} shader. Error info: ${gl.getShaderInfoLog(shader)}`
);
}
return shader;
}
}
export class WebGLCanvas extends WebGLBase {
canvas: HTMLCanvasElement;
gl: WebGLRenderingContext;
constructor(canvas?: HTMLCanvasElement) {
super();
this.canvas = canvas ?? document.createElement('canvas');
this.gl = this.canvas.getContext('webgl')!;
}
}
export class WebGL2Canvas extends WebGLBase {
canvas: HTMLCanvasElement;
gl: WebGL2RenderingContext;
constructor(canvas?: HTMLCanvasElement) {
super();
this.canvas = canvas ?? document.createElement('canvas');
this.gl = this.canvas.getContext('webgl2')!;
}
vs(vs: string): void {
if (!vs.startsWith('#version 300 es')) {
this.vsSource = `#version 300 es\n` + vs;
} else {
this.vsSource = vs;
}
}
fs(fs: string): void {
if (!fs.startsWith('#version 300 es')) {
this.fsSource = `#version 300 es\n` + fs;
} else {
this.vsSource = fs;
}
}
}
export function loadShader(
gl: WebGLRenderingContext,
type: number,
source: string
) {
const shader = gl.createShader(type)!;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
logger.error(
10,
type === gl.VERTEX_SHADER ? 'vertex' : 'fragment',
gl.getShaderInfoLog(shader) ?? ''
);
}
return shader;
}
export function createProgram(
gl: WebGLRenderingContext,
vsSource: string,
fsSource: string
) {
const vs = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fs = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const program = gl.createProgram()!;
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
logger.error(9, gl.getProgramInfoLog(program) ?? '');
}
return program;
}

View File

@ -8,12 +8,8 @@ export * as UI from './ui';
export * as Components from './components';
export * from './preset';
export * from './tools';
export * from './fx';
export * from './animateController';
export * from './controller';
export * from './danmaku';
// export * from './mark';
export * from './setting';
export * from './use';
export * from './utils';

View File

@ -1,47 +0,0 @@
import { Danmaku } from '../danmaku';
import { Component, h } from 'vue';
import { mainSetting } from './settingIns';
import { getIconHeight } from '../utils';
import { BoxAnimate } from '../components';
// 图标类型
Danmaku.registerSpecContent('i', content => {
const height = getIconHeight(content as AllIds);
return h(BoxAnimate as Component, {
id: content,
noborder: true,
noAnimate: true,
width: 32,
height
});
});
if (import.meta.env.DEV) {
Danmaku.backend = `/danmaku`;
}
export function createDanmaku() {
const { hook } = Mota.require('@user/data-base');
hook.once('reset', () => {
Danmaku.fetch();
});
// 勇士移动后显示弹幕
hook.on('moveOneStep', (x, y, floor) => {
const enabled = mainSetting.getValue('ui.danmaku', true);
if (!enabled) return;
const f = Danmaku.allInPos[floor];
if (f) {
const danmaku = f[`${x},${y}`];
if (danmaku) {
danmaku.forEach(v => {
setTimeout(() => {
v.show();
}, Math.random() * 1000);
});
}
}
});
}

View File

@ -1,69 +0,0 @@
import { debounce } from 'lodash-es';
import { ref } from 'vue';
import { sleep } from 'mutate-animate';
const close = ref(false);
let cx = 0;
let cy = 0;
/**
*
*/
const showFixed = debounce((block: Block) => {
const e = core.material.enemys[block.event.id as EnemyIds];
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 },
{ close: closeFixed }
);
}, 200);
/**
*
*/
const closeFixed = () => {
close.value = true;
sleep(200).then(() => {
const { fixedUi } = Mota.require('@motajs/legacy-ui');
fixedUi.closeByName('fixed');
close.value = false;
});
};
// todo: 应当在这里实现查看临界与特殊属性的功能
export let hovered: Block | null;
export function createFixed() {
const { hook, gameListener } = Mota.require('@user/data-base');
gameListener.on('hoverBlock', block => {
closeFixed();
hovered = block;
});
gameListener.on('leaveBlock', (_, __, leaveGame) => {
showFixed.cancel();
if (!leaveGame) closeFixed();
hovered = null;
});
gameListener.on('mouseMove', e => {
cx = e.clientX;
cy = e.clientY;
showFixed.cancel();
if (hovered) {
showFixed(hovered);
}
});
hook.once('mounted', () => {
const { mainUi } = Mota.require('@motajs/legacy-ui');
mainUi.on('start', () => {
showFixed.cancel();
closeFixed();
});
});
}

View File

@ -1,18 +1,12 @@
import { createDanmaku } from './danmaku';
import { createFixed } from './fixed';
import { createSetting, createUI } from './ui';
export function createPreset() {
createDanmaku();
createFixed();
createUI();
createSetting();
}
export * from './ui';
export * from './settings';
export * from './danmaku';
export * from './fixed';
export * from './keyboard';
export * from './uiIns';
export * from './settingIns';

View File

@ -8,26 +8,13 @@ mainUi.register(
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('virtualKey', VirtualKey)
);
mainUi.showAll();
export const fixedUi = new UiController(true);
fixedUi.register(
new GameUi('fixed', UI.Fixed),
new GameUi('chapter', UI.Chapter),
new GameUi('start', UI.Start),
new GameUi('load', UI.Load),
new GameUi('danmaku', UI.Danmaku),
new GameUi('danmakuEditor', UI.DanmakuEditor),
new GameUi('tips', UI.Tips)
);
fixedUi.register(new GameUi('load', UI.Load));
fixedUi.showAll();

View File

@ -1,8 +1,3 @@
// import { init } from './achievement';
// init();
// export * from './achievement';
export * from './book';
export * from './common';
export * from './equipbox';

View File

@ -1,143 +0,0 @@
<template>
<div id="chapter">
<canvas id="chapter-back"></canvas>
<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 { IMountedVBind } from '../interface';
import { isNil } from 'lodash-es';
const props = defineProps<IMountedVBind>();
let can: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D;
let text: HTMLSpanElement;
onMounted(async () => {
can = document.getElementById('chapter-back') as HTMLCanvasElement;
ctx = can.getContext('2d')!;
text = document.getElementById('chapter-text') as HTMLSpanElement;
const ani = new Animation();
const w = window.innerWidth * devicePixelRatio;
const h = window.innerHeight * devicePixelRatio;
ctx.font = '5vh normal';
const textWidth = ctx.measureText(props.chapter).width;
const line = h * 0.05;
ani.register('rect', 0);
ani.register('line', -10);
ani.register('lineOpacity', 1);
ani.register('rect2', h / 2);
ani.register('text', window.innerWidth + 10 + textWidth);
can.width = w;
can.height = h;
can.style.width = `${window.innerWidth}px`;
can.style.height = `${window.innerHeight}px`;
text.style.left = `${w + 10}px`;
text.style.width = `${textWidth}px`;
let soundPlayed = false;
let started = false;
ani.ticker.add(time => {
if (isNil(time) || isNaN(time)) return;
if (!started) {
started = true;
return;
}
if (time >= 4050) {
props.controller.close(props.num);
ani.ticker.destroy();
}
if (!soundPlayed && time >= 1500) {
soundPlayed = true;
core.playSound('chapter.opus');
}
ctx.restore();
ctx.save();
text.style.left = `${ani.value.text}px`;
ctx.fillStyle = '#000';
ctx.clearRect(0, 0, w, h);
if (time <= 2000) {
ctx.fillRect(0, h / 2, w, -ani.value.rect);
ctx.fillRect(0, h / 2, w, ani.value.rect);
} else if (time >= 2000 && time <= 3050) {
ctx.fillRect(0, 0, w, ani.value.rect2);
ctx.fillRect(0, h, w, -ani.value.rect2);
}
ctx.shadowColor = '#fff';
ctx.shadowBlur = 3;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.lineWidth = 3;
ctx.strokeStyle = '#fff';
ctx.fillStyle = '#fff';
ctx.globalAlpha = ani.value.lineOpacity;
ctx.beginPath();
ctx.moveTo(0, h / 2 - line);
ctx.lineTo(ani.value.line, h / 2 - line);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(w, h / 2 + line);
ctx.lineTo(w - ani.value.line, h / 2 + line);
ctx.stroke();
ctx.shadowBlur = 0;
ctx.filter = 'blur(5px)';
ctx.beginPath();
ctx.arc(ani.value.line, h / 2 - line, 10, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(w - ani.value.line, h / 2 + line, 10, 0, Math.PI * 2);
ctx.fill();
});
ani.mode(hyper('tan', 'center'))
.time(3000)
.absolute()
.apply('line', w + 10)
.mode(hyper('sin', 'out'))
.time(1000)
.apply('rect', h / 2)
.mode(hyper('tan', 'center'))
.time(3000)
.apply('text', -textWidth * 2 - 10);
await sleep(2000);
ani.mode(hyper('sin', 'in')).time(1000).apply('rect2', 0);
await sleep(1000);
ani.mode(hyper('sin', 'out')).time(1000).apply('lineOpacity', 0);
});
</script>
<style lang="less" scoped>
#chapter {
width: 100vw;
height: 100vh;
position: fixed;
left: 0;
top: 0;
user-select: none;
display: flex;
align-items: center;
}
#chapter-back {
position: fixed;
width: 100%;
height: 100%;
}
#chapter-text {
position: relative;
font-family: 'normal';
font-size: 5vh;
text-shadow: 0px 0px 5px #fff;
}
</style>

View File

@ -1,225 +0,0 @@
<template>
<div id="danmaku-div">
<div
:id="`danmaku-${one.id}`"
class="danmaku-one"
v-for="one of Danmaku.showList"
:key="one.id"
@mouseenter="mousein(one.id)"
@mouseleave="mouseleave(one.id)"
@touchstart="touchStart(one.id)"
>
<span class="danmaku-info">
<span
class="danmaku-like-icon"
:liked="likedMap[one.id] < 0"
@click="postLike(one)"
>
<like-filled />
</span>
<span class="danmaku-like-num">{{
Math.abs(likedMap[one.id])
}}</span>
</span>
<component :is="one.getVNode()"></component>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onUnmounted, reactive, watch } from 'vue';
import { Danmaku } from '../danmaku';
import { LikeFilled } from '@ant-design/icons-vue';
import { mainSetting } from '../preset/settingIns';
import { debounce } from 'lodash-es';
interface ElementMap {
ele: HTMLDivElement;
danmaku: Danmaku;
hover: boolean;
top: number;
style: CSSStyleDeclaration;
}
const likedMap: Record<number, number> = reactive({});
const map = Danmaku.showMap;
const eleMap: Map<number, ElementMap> = new Map();
const liked = reactive<Record<number, boolean>>({});
const speed = mainSetting.getValue('ui.danmakuSpeed', 60);
const likeFn = (l: boolean, d: Danmaku) => {
liked[d.id] = l;
};
watch(Danmaku.showList, list => {
list.forEach(v => {
if (!eleMap.has(v.id)) {
nextTick(() => {
liked[v.id] = v.liked;
v.on('like', likeFn);
requestAnimationFrame(() => {
addElement(v.id);
});
});
}
});
});
function addElement(id: number) {
const danmaku = map.get(id);
if (!danmaku) return;
const div = document.getElementById(`danmaku-${id}`);
if (!div) return;
const ele: ElementMap = {
danmaku,
ele: div as HTMLDivElement,
hover: false,
top: -1,
style: getComputedStyle(div)
};
div.style.setProperty('--end', `${-div.scrollWidth}px`);
div.style.setProperty(
'--duration',
`${Math.floor((window.innerWidth + div.scrollWidth + 100) / speed)}s`
);
div.style.setProperty('left', ele.style.left);
div.addEventListener('animationend', () => {
danmaku.showEnd();
eleMap.delete(id);
delete likedMap[id];
});
eleMap.set(id, ele);
likedMap[id] = danmaku.liked ? -danmaku.likedNum : danmaku.likedNum;
calTop(id);
}
function mousein(id: number) {
const danmaku = eleMap.get(id)!;
danmaku.hover = true;
danmaku.ele.classList.add('danmaku-paused');
}
function mouseleave(id: number) {
const danmaku = eleMap.get(id)!;
danmaku.hover = false;
danmaku.ele.classList.remove('danmaku-paused');
}
const touchDebounce = debounce(mouseleave, 3000);
function touchStart(id: number) {
mousein(id);
touchDebounce(id);
}
function calTop(id: number) {
const danmaku = eleMap.get(id)!;
const fontSize = mainSetting.getValue('screen.fontSize', 16) * 1.25 + 15;
const used: Set<number> = new Set();
eleMap.forEach(v => {
const { ele, style } = v;
const pos = parseInt(style.transform.slice(19, -4));
const width = ele.scrollWidth;
if (
pos <= window.innerWidth + 200 &&
pos + width >= window.innerWidth
) {
used.add(v.top);
}
});
let i = -1;
danmaku.top = 0;
danmaku.ele.style.top = `20px`;
while (++i < 20) {
if (!used.has(i)) {
danmaku.top = i;
danmaku.ele.style.top = `${fontSize * i + 20}px`;
break;
}
}
}
async function postLike(danmaku: Danmaku) {
const res = await danmaku.triggerLike();
const liked = res.data.liked;
likedMap[danmaku.id] = liked ? -danmaku.likedNum : danmaku.likedNum;
}
onUnmounted(() => {});
</script>
<style lang="less" scoped>
#danmaku-div {
position: fixed;
width: 0;
height: 0;
left: 0;
top: 0;
overflow: visible;
font-size: 150%;
display: flex;
align-items: center;
z-index: 100;
}
.danmaku-one {
--end: 0;
--duration: 5s;
position: fixed;
left: 0;
transform: translateX(100vw);
width: max-content;
white-space: nowrap;
text-wrap: nowrap;
padding: 0 5px;
display: flex;
align-items: center;
animation: danmaku-roll linear var(--duration) forwards;
animation-play-state: running;
}
.danmaku-one:hover {
background-color: #0006;
border-radius: 5px;
animation-play-state: paused;
}
.danmaku-one .danmaku-paused {
animation-play-state: paused;
}
@keyframes danmaku-roll {
0% {
transform: translateX(100vw);
}
100% {
transform: translateX(var(--end));
}
}
.danmaku-info {
text-shadow: 1px 1px 1px black, 1px -1px 1px black, -1px 1px 1px black,
-1px -1px 1px black;
}
.danmaku-like-num {
font-size: 75%;
}
.danmaku-like-icon {
transition: color 0.1s linear;
color: white;
}
.danmaku-like-icon[liked='true'],
.danmaku-like-icon:hover {
color: aqua;
}
</style>

View File

@ -1,671 +0,0 @@
<template>
<div id="danmaku-editor" @click.capture="clickInter">
<div id="danmaku-input">
<span
class="danmaku-tool"
:open="cssOpened"
@click="openTool('css')"
>CSS</span
>
<span
class="danmaku-tool"
:open="fillOpened"
@click="openTool('fillColor')"
>
<font-colors-outlined />
</span>
<span
class="danmaku-tool"
:open="strokeOpened"
@click="openTool('strokeColor')"
>
<highlight-outlined />
</span>
<span
class="danmaku-tool"
:open="iconOpened"
@click="openTool('icon')"
>
<meh-outlined />
</span>
<div id="danmaku-input-div">
<Input
id="danmaku-input-input"
v-model:value="inputValue"
:max-length="200"
placeholder="请在此输入弹幕,显示中括号请使用\[或\]"
autocomplete="off"
@change="input(inputValue)"
@press-enter="inputEnter()"
/>
</div>
<span
class="danmaku-tool danmaku-post"
:posting="posting"
@click="send()"
>
<send-outlined />
</span>
</div>
<Transition name="danmaku">
<div v-if="cssOpened" id="danmaku-css">
<span id="danmaku-css-hint">编辑弹幕的 CSS 样式</span>
<Input
id="danmaku-css-input"
v-model:value="cssInfo"
:max-length="300"
placeholder="请在此输入样式"
autocomplete="off"
@blur="inputCSS(cssInfo)"
@press-enter="inputCSS(cssInfo)"
/>
<span v-if="cssError" id="danmaku-css-error">{{
cssError
}}</span>
</div>
<div v-else-if="iconOpened" id="danmaku-icon">
<span id="danmaku-icon-hint">常用图标</span>
<Scroll
class="danmaku-icon-scroll"
:no-scroll="true"
type="horizontal"
>
<div id="danmaku-icon-div">
<span
class="danmaku-icon-one"
v-for="icon of frequentlyIcon"
@click="addIcon(icon as AllIds)"
>
<BoxAnimate
:id="icon as AllIds"
:noborder="true"
:no-animate="true"
:height="getIconHeight(icon as AllIds)"
></BoxAnimate>
</span>
</div>
</Scroll>
<span
id="danmaku-icon-all"
class="button-text"
:active="iconAll"
@click="iconAll = !iconAll"
>
所有图标 <up-outlined />
</span>
</div>
<div v-else-if="fillOpened || strokeOpened" id="danmaku-color">
<span id="danmaku-color-hint"
>设置{{ fillOpened ? '填充' : '描边' }}颜色</span
>
<Scroll
class="danmaku-color-scroll"
:no-scroll="true"
type="horizontal"
>
<div id="danmaku-color-container">
<span
v-for="color of frequentlyColor"
:style="{ backgroundColor: color }"
:selected="color === nowColor"
@click="inputColor(color)"
class="danmaku-color-one"
></span>
</div>
</Scroll>
<Input
id="danmaku-color-input"
:max-length="100"
v-model:value="nowColor"
placeholder="输入颜色"
autocomplete="off"
@blur="inputColor(nowColor)"
@pressEnter="inputColor(nowColor)"
></Input>
</div>
</Transition>
<Transition name="danmaku-icon">
<div v-if="iconAll" id="danmaku-icon-all-div">
<span
>本列表不包含额外素材如果需要额外素材请手动填写素材id</span
>
<Scroll class="danmaku-all-scroll">
<div id="danmaku-all-container">
<span
v-for="icon of getAllIcons()"
@click="addIcon(icon)"
>
<BoxAnimate
:id="icon"
:height="getIconHeight(icon)"
:no-animate="true"
:noborder="true"
></BoxAnimate>
</span>
</div>
</Scroll>
</div>
</Transition>
</div>
</template>
<script lang="ts" setup>
import { Ref, onMounted, onUnmounted, ref } from 'vue';
import {
FontColorsOutlined,
HighlightOutlined,
MehOutlined,
SendOutlined,
UpOutlined
} from '@ant-design/icons-vue';
import { Danmaku } from '../danmaku';
import { sleep } from 'mutate-animate';
import { calStringSize, stringifyCSS, parseCss, getIconHeight } from '../utils';
import { gameKey } from '@motajs/system-action';
import { isNil } from 'lodash-es';
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';
import { Input } from 'ant-design-vue';
const props = defineProps<IMountedVBind>();
const frequentlyIcon: (AllIds | 'hero' | `X${number}`)[] = [
'hero',
'yellowKey',
'blueKey',
'redKey',
'A492',
'A494',
'A497',
'redPotion',
'redGem',
'blueGem',
'I559',
'X10194',
'downPortal',
'leftPortal',
'upPortal',
'rightPortal',
'upFloor',
'downFloor',
'greenSlime',
'yellowKnight',
'bat',
'slimelord'
];
const frequentlyColor: string[] = [
'#ffffff',
'#000000',
'#ff0000',
'#00ff00',
'#0000ff',
'#ffff00',
'#00ffff',
'#ff00ff',
'#c0c0c0',
'#808080',
'#800000',
'#800080',
'#008000',
'#808000',
'#000080',
'#008080'
];
let mainDiv: HTMLDivElement;
let danmaku = Danmaku.lastEditoredDanmaku ?? new Danmaku();
const cssOpened = ref(false);
const iconOpened = ref(false);
const fillOpened = ref(false);
const strokeOpened = ref(false);
const posting = ref(false);
const iconAll = ref(false);
const nowColor = ref('#ffffff');
const inputValue = ref(danmaku.text);
const cssInfo = ref(stringifyCSS(danmaku.style));
const cssError = ref('');
const map: Record<string, Ref<boolean>> = {
css: cssOpened,
icon: iconOpened,
fillColor: fillOpened,
strokeColor: strokeOpened
};
function openTool(tool: string) {
iconAll.value = false;
for (const [key, value] of Object.entries(map)) {
if (key === tool) {
value.value = !value.value;
} else {
value.value = false;
}
}
if (tool === 'fillColor') {
nowColor.value = danmaku.textColor;
} else if (tool === 'strokeColor') {
nowColor.value = danmaku.strokeColor;
}
}
function send() {
if (posting.value) return;
if (danmaku.text === '') {
tip('warning', '请填写弹幕!');
return;
}
if (!core.isPlaying()) {
tip('warning', '请进入游戏后再发送弹幕');
return;
}
if (calStringSize(danmaku.text) > 200) {
tip('warning', '弹幕长度超限!');
return;
}
const { x, y } = core.status.hero.loc;
const floor = core.status.floorId;
if (isNil(x) || isNil(y) || isNil(floor)) {
tip('warning', '当前无法发送弹幕');
return;
}
danmaku.x = x;
danmaku.y = y;
danmaku.floor = floor;
danmaku
.post()
.then(value => {
if (value.data.code === 0) {
danmaku.show();
danmaku = new Danmaku();
inputValue.value = '';
cssInfo.value = '';
}
})
.finally(() => {
posting.value = false;
});
}
function close() {
mainDiv.classList.remove('danmaku-startup');
mainDiv.classList.add('danmaku-close');
sleep(200).then(() => {
props.controller.close(props.num);
});
}
function input(value: string) {
const size = calStringSize(value);
if (size > 200) {
tip('warning', '弹幕长度超限!');
}
const before = danmaku.text;
const { info, ret } = logger.catch(() => {
danmaku.text = value;
return danmaku.parse();
});
if (info.length > 0) {
if (info[0].code === 4) {
tip('error', '请检查中括号匹配');
danmaku.text = before;
} else {
danmaku.vNode = ret;
}
}
}
function inputEnter() {
input(inputValue.value);
inputCSS(cssInfo.value);
send();
}
function inputCSS(text: string) {
const { info, ret } = logger.catch(() => {
return parseCss(text);
});
if (info.some(v => v.level > LogLevel.LOG)) {
cssError.value = '语法错误';
return;
}
const allow = Danmaku.checkCSSAllow(ret);
if (allow.length > 0) {
cssError.value = allow[0];
return;
} else {
cssError.value = '';
}
danmaku.css(ret);
}
function inputColor(color: string) {
nowColor.value = color;
if (fillOpened.value) {
danmaku.textColor = color;
} else {
danmaku.strokeColor = color;
}
}
function addIcon(icon: AllIds | 'hero') {
const iconText = `[i:${icon}]`;
if (iconText.length + danmaku.text.length > 200) {
tip('warn', '弹幕长度超限!');
return;
}
danmaku.text += iconText;
inputValue.value = danmaku.text;
}
function getAllIcons() {
return [
...new Set(
Object.values(core.maps.blocksInfo)
.filter(v => v.cls !== 'tileset')
.map(v => {
return v.id;
})
)
];
}
let clickIn = false;
let closed = false;
function clickOuter() {
requestAnimationFrame(() => {
if (!clickIn) {
if (!iconAll.value) {
if (!closed) {
closed = true;
close();
}
} else iconAll.value = false;
}
clickIn = false;
});
}
function clickInter() {
clickIn = true;
}
let lockedBefore = false;
onMounted(() => {
mainDiv = document.getElementById('danmaku-editor') as HTMLDivElement;
mainDiv.classList.add('danmaku-startup');
gameKey.disable();
core.lockControl();
mainDiv.addEventListener('focus', () => {
lockedBefore = core.status.lockControl;
core.lockControl();
gameKey.disable();
});
mainDiv.addEventListener('blur', () => {
gameKey.enable();
if (!lockedBefore) core.unlockControl();
});
document.addEventListener('click', clickOuter, { capture: true });
document.addEventListener('touchend', clickOuter, { capture: true });
});
onUnmounted(() => {
if (danmaku.text !== '' || Object.keys(danmaku.style).length > 0) {
Danmaku.lastEditoredDanmaku = danmaku;
} else {
delete Danmaku.lastEditoredDanmaku;
}
if (!lockedBefore) core.unlockControl();
gameKey.enable();
document.removeEventListener('click', clickOuter);
document.removeEventListener('touchend', clickOuter);
});
</script>
<style lang="less" scoped>
#danmaku-editor {
position: fixed;
width: 100%;
bottom: 10px;
display: flex;
align-items: center;
flex-direction: column-reverse;
justify-content: end;
font-size: 200%;
background-color: #000b;
}
.danmaku-startup {
animation: editor-startup 0.2s ease-out 0s 1 normal forwards running;
}
.danmaku-close {
animation: editor-close 0.2s ease-in 0s 1 normal forwards running;
}
@keyframes editor-startup {
0% {
transform: translateY(70px);
}
100% {
transform: none;
}
}
@keyframes editor-close {
0% {
transform: none;
}
100% {
transform: translateY(110px);
}
}
#danmaku-input {
height: 40px;
width: 60%;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 10px;
margin-top: 10px;
}
#danmaku-input-div {
width: 100%;
height: 100%;
display: flex;
align-items: center;
#danmaku-input-input {
width: 100%;
height: 100%;
font-size: 80%;
}
}
.danmaku-tool {
cursor: pointer;
color: white;
transition: color 0.2s linear;
margin-right: 7px;
font-family: 'FiraCode', 'Arial';
}
.danmaku-tool[open='true'],
.danmaku-tool:hover {
color: aqua;
}
.danmaku-post {
margin-left: 7px;
}
.danmaku-post[posting='true'] {
color: gray;
cursor: wait;
}
#danmaku-css {
width: 60%;
font-size: 60%;
display: flex;
flex-direction: row;
align-items: center;
white-space: nowrap;
font-family: 'FiraCode', 'Arial';
#danmaku-css-input {
width: 100%;
height: 100%;
font-size: 80%;
margin: 0 10px;
}
#danmaku-css-error {
color: lightcoral;
}
}
#danmaku-icon {
width: 60%;
display: flex;
align-items: center;
font-size: 80%;
white-space: nowrap;
justify-content: space-between;
font-family: 'FiraCode', 'Arial';
.danmaku-icon-scroll {
width: calc(90% - 200px);
}
#danmaku-icon-div {
display: flex;
align-items: center;
width: 100%;
}
.danmaku-icon-one {
display: flex;
align-items: center;
}
}
#danmaku-icon-all-div {
position: fixed;
bottom: 110px;
height: 50vh;
background-color: #000b;
border-radius: 20px;
padding: 1%;
font-size: 75%;
width: 60%;
display: flex;
flex-direction: column;
align-items: center;
color: #dddd;
#danmaku-all-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.danmaku-all-scroll {
height: calc(100% - 70px);
}
}
#danmaku-color {
width: 60%;
display: flex;
align-items: center;
white-space: nowrap;
justify-content: space-between;
font-family: 'FiraCode', 'Arial';
font-size: 75%;
#danmaku-color-container {
display: flex;
align-items: center;
margin: 10px;
width: 100%;
}
.danmaku-color-one {
width: 30px;
min-width: 30px;
height: 20px;
border-radius: 6px;
margin-right: 7px;
border: 2px solid transparent;
transition: border 0.1s linear;
}
.danmaku-color-one[selected='true'],
.danmaku-color-one:hover {
border: 2px solid gold;
}
.danmaku-color-scroll {
width: calc(100% - 400px);
}
#danmaku-color-input {
width: 200px;
}
}
.danmaku-enter-active,
.danmaku-leave-active {
transition: all 0.4s ease-out;
position: absolute;
bottom: 50px;
}
.danmaku-enter-from,
.danmaku-leave-to {
opacity: 0;
transform: translateY(50px);
}
.danmaku-icon-enter-active,
.danmaku-icon-leave-active {
transition: all 0.3s ease-out;
}
.danmaku-icon-enter-from,
.danmaku-icon-leave-to {
opacity: 0;
transform: translateY(50px);
}
@media screen and (max-width: 600px) {
#danmaku-input {
width: 90%;
}
#danmaku-css {
width: 90%;
}
#danmaku-icon {
width: 90%;
}
}
</style>

View File

@ -1,75 +0,0 @@
<template>
<Colomn @close="exit" :width="80" :height="80" :left="30" :right="70"
><template #left
><div id="desc-list">
<div
v-for="(data, k) in desc"
class="selectable desc-item"
:selected="selected === k"
:show="show(data.condition)"
@click="click(k)"
>
<span v-if="show(data.condition)">{{ data.text }}</span>
</div>
</div></template
>
<template #right><span v-html="content"></span></template
></Colomn>
</template>
<script lang="ts" setup>
import { computed, onUnmounted, ref } from 'vue';
import desc from '../data/desc.json';
import { splitText } from '../utils';
import Colomn from '../components/colomn.vue';
import { gameKey } from '@motajs/system-action';
import { IMountedVBind } from '../interface';
const props = defineProps<IMountedVBind>();
type DescKey = keyof typeof desc;
const selected = ref(Object.keys(desc)[0] as DescKey);
function exit() {
props.controller.close(props.num);
}
const content = computed(() => {
return eval('`' + splitText(desc[selected.value].desc) + '`');
});
function click(key: DescKey) {
if (!eval(desc[key].condition)) return;
selected.value = key;
}
function show(condition: string) {
return eval(condition);
}
gameKey.use(props.ui.symbol);
gameKey
.realize('exit', () => {
exit();
})
.realize('desc', () => {
exit();
});
onUnmounted(() => {
gameKey.dispose(props.ui.symbol);
});
</script>
<style lang="less" scoped>
#desc-list {
display: flex;
flex-direction: column;
}
.desc-item[show='false'] {
margin: 0;
padding: 0;
}
</style>

View File

@ -1,174 +0,0 @@
<template>
<div id="fixed">
<Box
v-model:height="height"
v-model:left="left"
v-model:top="top"
v-model:width="width"
>
<div id="enemy-fixed">
<span id="enemy-name">{{ enemy.enemy.name }}</span>
<div id="enemy-special">
<span
v-for="(text, i) of special"
:style="{ color: text[1] }"
>{{ text[0] }}</span
>
</div>
<div class="enemy-attr" v-for="(a, i) of detail">
<span class="attr-name" :style="{ color: a[2] }">{{
a[1]
}}</span>
<span class="attr-value" :style="{ color: a[2] }">{{
format(a[0])
}}</span>
</div>
</div>
</Box>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUpdated, Ref, ref, watch } from 'vue';
import Box from '../components/box.vue';
import { nextFrame } from '../utils';
import { EnemyInfo, IDamageEnemy } from '@motajs/types';
const props = defineProps<{
num: number;
enemy: IDamageEnemy;
close: Ref<boolean>;
loc: [x: number, y: number];
}>();
const emits = defineEmits<{
(e: 'close'): void;
}>();
watch(props.close, n => {
if (n) {
fixed.style.opacity = '0';
}
});
let main: HTMLDivElement;
let fixed: HTMLDivElement;
const format = core.formatBigNumber;
const enemy = props.enemy;
const detail = ((): [number, string, string][] => {
const info = enemy.info;
const data = enemy.calCritical()[0];
const ratio = core.status.thisMap?.ratio ?? 1;
return [
[info.hp, '生命', 'lightgreen'],
[info.atk, '攻击', 'lightcoral'],
[info.def, '防御', 'lightblue'],
[enemy.enemy.money, '金币', 'lightyellow'],
[enemy.enemy.exp, '经验', 'lawgreen'],
[data?.atkDelta ?? 0, '临界', 'lightsalmon'],
[data?.delta ?? 0, '临界减伤', 'lightpink'],
[enemy.calDefDamage(ratio).delta, `${ratio}`, 'cyan']
];
})();
const special = (() => {
const s = [...enemy.info.special];
const fromFunc = (
func: string | ((enemy: EnemyInfo) => string),
enemy: EnemyInfo
) => {
return typeof func === 'string' ? func : func(enemy);
};
const show = s.slice(0, 2).map(v => {
const s = Mota.require('@user/data-state').specials[v];
return [fromFunc(s.name, enemy.info), s.color];
});
if (s.length > 2) show.push(['...', 'white']);
return show;
})();
const left = ref(0);
const top = ref(0);
const width = ref(300);
const height = ref(400);
let vh = window.innerHeight;
let vw = window.innerWidth;
async function calHeight() {
vh = window.innerHeight;
vw = window.innerWidth;
width.value = vh * 0.28;
await new Promise(res => requestAnimationFrame(res));
updateMain();
if (!main) return;
const style = getComputedStyle(main);
const h = parseFloat(style.height);
const [cx, cy] = props.loc;
if (cy + h + 10 > vh - 10) top.value = vh - h - 10;
else top.value = cy + 10;
if (cx + width.value + 10 > vw - 10) left.value = vw - width.value - 10;
else left.value = cx + 10;
height.value = h;
}
function updateMain() {
main = document.getElementById('enemy-fixed') as HTMLDivElement;
fixed = document.getElementById('fixed') as HTMLDivElement;
if (main) {
main.addEventListener('mouseleave', () => emits('close'));
}
nextFrame(() => {
fixed.style.opacity = '1';
});
}
onUpdated(calHeight);
onMounted(() => {
updateMain();
calHeight();
});
</script>
<style lang="less" scoped>
#fixed {
font-family: 'normal';
font-size: 2.5vh;
opacity: 0;
transition: opacity 0.2s linear;
}
#enemy-fixed {
display: flex;
flex-direction: column;
align-items: center;
background-color: #000c;
padding: 1vh;
}
#enemy-special {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-around;
}
.enemy-attr {
display: flex;
flex-direction: row;
width: 100%;
}
.attr-name {
flex-basis: 50%;
width: 100%;
text-align: right;
padding-right: 5%;
}
.attr-value {
flex-basis: 50%;
padding-left: 5%;
}
</style>

View File

@ -1,47 +0,0 @@
<template>
<div id="fixed-detail">
<BookDetail
:from-book="false"
:default-panel="panel"
@close="close"
></BookDetail>
</div>
</template>
<script lang="ts" setup>
import { getDetailedEnemy } from '../tools/fixed';
import BookDetail from './bookDetail.vue';
import { detailInfo } from '../tools/book';
import { hovered } from '../preset/fixed';
import { IMountedVBind } from '../interface';
const props = defineProps<IMountedVBind>();
const panel = props.panel ?? 'special';
detailInfo.pos = 0;
if (hovered) {
const { x, y } = hovered;
const enemy = core.status.thisMap.enemy.get(x, y);
if (enemy) {
const detail = getDetailedEnemy(enemy);
detailInfo.enemy = detail;
} else {
close();
}
} else {
close();
}
function close() {
props.controller.close(props.num);
}
</script>
<style lang="less" scoped>
#fixed-detail {
width: 80%;
height: 100%;
}
</style>

View File

@ -1,19 +1,9 @@
export { default as Book } from './book.vue';
export { default as BookDetail } from './bookDetail.vue';
export { default as Chapter } from './chapter.vue';
export { default as Desc } from './desc.vue';
export { default as Equipbox } from './equipbox.vue';
export { default as Fixed } from './fixed.vue';
export { default as FixedDetail } from './fixedDetail.vue';
export { default as Fly } from './fly.vue';
export { default as Settings } from './settings.vue';
export { default as Shop } from './shop.vue';
export { default as Skill } from './skill.vue';
export { default as SkillTree } from './skillTree.vue';
export { default as Start } from './start.vue';
export { default as Toolbox } from './toolbox.vue';
export { default as Hotkey } from './hotkey.vue';
export { default as Load } from './load.vue';
export { default as Danmaku } from './danmaku.vue';
export { default as DanmakuEditor } from './danmakuEditor.vue';
export { default as Tips } from './tips.vue';

View File

@ -1,79 +0,0 @@
<template>
<Column @close="exit" :width="70" :height="70"
><template #left
><div id="skill-list">
<span
v-for="(v, k) in skills"
class="selectable skill-item"
:selected="k === selected"
:selectable="skillOpened(k)"
@click="select(k)"
>{{ v.text }}</span
>
</div></template
>
<template #right><span v-html="content"></span></template
></Column>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import skills from '../data/skill.json';
import Column from '../components/colomn.vue';
import { IMountedVBind } from '../interface';
import { isNil } from 'lodash-es';
const props = defineProps<IMountedVBind>();
type Skills = keyof typeof skills;
const selected = ref<Skills>('none');
function skillOpened(skill: Skills) {
return eval(skills[skill].opened) as boolean;
}
function select(skill: Skills) {
if (!skillOpened(skill)) return;
selected.value = skill;
}
const content = computed(() => {
return eval(
'`' +
skills[selected.value].desc
.map((v, i, a) => {
if (/^\d+\./.test(v)) return `${'&nbsp;'.repeat(12)}${v}`;
else if (
(!isNil(a[i - 1]) &&
v !== '<br>' &&
a[i - 1] === '<br>') ||
i === 0
) {
return `${'&nbsp;'.repeat(8)}${v}`;
} else return v;
})
.join('')
.replace(
/level:(\d+)/g,
'Mota.require("@user/data-state").getSkillLevel($1)'
) +
'`'
);
});
function exit() {
props.controller.close(props.num);
}
</script>
<style lang="less" scoped>
#skill-list {
display: flex;
flex-direction: column;
}
.skill-item[selectable='false'] {
color: gray;
}
</style>

View File

@ -1,449 +0,0 @@
<template>
<div id="skill-tree">
<div id="tools">
<span id="back" class="button-text tools" @click="exit"
><left-outlined />返回游戏</span
>
</div>
<span id="skill-title">{{ skill.title }}</span>
<Divider dashed style="border-color: #ddd4" id="divider"></Divider>
<div id="skill-info">
<Scroll id="skill-desc" :no-scroll="true">
<span v-html="desc"></span>
</Scroll>
<div id="skill-effect">
<span v-if="level > 0" v-html="effect[0]"></span>
<span v-if="level < skill.max" v-html="effect[1]"></span>
</div>
</div>
<Divider
dashed
style="border-color: #ddd4"
id="divider-split"
></Divider>
<div id="skill-bottom">
<canvas id="skill-canvas"></canvas>
<Divider
dashed
style="border-color: #ddd4"
:type="isMobile ? 'horizontal' : 'vertical'"
id="divider-vertical"
></Divider>
<div id="skill-upgrade-info">
<div id="skill-upgrade-up">
<span id="skill-level"
>当前等级{{ level }} / {{ skill.max }}</span
>
<Divider dashed class="upgrade-divider"></Divider>
<span
v-if="level < skill.max"
id="skill-consume"
:style="{ color: consume <= mdef ? '#fff' : '#f44' }"
>升级花费{{ consume }}</span
>
<span v-else id="skill-consume" style="color: gold"
>已满级</span
>
<Divider dashed class="upgrade-divider"></Divider>
<Scroll id="front-scroll" :no-scroll="true"
><div id="skill-front">
<span>前置技能</span>
<span
v-for="str of front"
:style="{
color: str.startsWith('a') ? '#fff' : '#f44'
}"
>{{ str.slice(1) }}</span
>
</div></Scroll
>
</div>
<div id="skill-upgrade-bottom">
<Divider dashed class="upgrade-divider"></Divider>
<div id="skill-chapter">
<span class="button-text" @click="selectChapter(-1)"
><LeftOutlined
/></span>
&nbsp;&nbsp;
<span>{{ chapterDict[chapter] }}</span>
&nbsp;&nbsp;
<span class="button-text" @click="selectChapter(1)"
><RightOutlined
/></span>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
import Scroll from '../components/scroll.vue';
import { splitText } from '../utils';
import { isMobile, tip } from '../use';
import { sleep } from 'mutate-animate';
import { gameKey } from '@motajs/system-action';
import { IMountedVBind } from '../interface';
import { isNil } from 'lodash-es';
import { Divider } from 'ant-design-vue';
const props = defineProps<IMountedVBind>();
const skillTree = Mota.require('@user/data-state');
let canvas: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D;
const chapterDict = {
chapter1: '第一章',
chapter2: '第二章'
};
const selected = ref(0);
const chapter = ref<keyof typeof chapterDict>('chapter1');
const update = ref(false);
flags.skillTree ??= 0;
const s = skillTree.skills;
const chapterList = Object.keys(s) as (keyof typeof chapterDict)[];
selected.value = s[chapterList[flags.skillTree]][0].index;
chapter.value = chapterList[flags.skillTree];
watch(selected, draw);
watch(update, () => (mdef.value = core.status.hero.mdef));
const mdef = ref(core.status.hero.mdef);
const skill = computed(() => {
update.value;
return skillTree.getSkillFromIndex(selected.value)!;
});
const skills = computed(() => {
return s[chapter.value];
});
const desc = computed(() => {
return eval(
'`' +
splitText(skill.value.desc).replace(/level(:\d+)?/g, (str, $1) => {
if ($1) return `skillTree.getSkillLevel(${$1})`;
else return `skillTree.getSkillLevel(${skill.value.index})`;
}) +
'`'
);
});
const effect = computed(() => {
return [0, 1].map(v => {
update.value = update.value;
const prefix = v === 0 ? '当前效果:' : '下一级效果:';
const level = skillTree.getSkillLevel(skill.value.index);
const content = skill.value.effect(level + v);
return prefix + content.join('');
}) as [string, string];
});
const dict = computed(() => {
const dict: Record<number, number> = {};
const all = skills.value;
all.forEach((v, i) => {
dict[v.index] = i;
});
return dict;
});
const front = computed(() => {
return skill.value.front.map(v => {
return `${skillTree.getSkillLevel(v[0]) >= v[1] ? 'a' : 'b'}${
v[1]
} ${skills.value[dict.value[v[0]]].title}`;
});
});
const consume = computed(() => {
update.value;
return skillTree.getSkillConsume(selected.value);
});
const level = computed(() => {
update.value;
return skillTree.getSkillLevel(selected.value);
});
function exit() {
props.controller.close(props.num);
}
function resize() {
canvas.width = canvas.scrollWidth * devicePixelRatio;
canvas.height = canvas.scrollHeight * devicePixelRatio;
}
function draw() {
const d = dict.value;
const w = canvas.width;
const per = w / 11;
ctx.clearRect(0, 0, canvas.width, canvas.height);
skills.value.forEach(v => {
const [x, y] = v.loc.map(v => v * 2 - 1);
// 线
v.front.forEach(([skill], i) => {
const s = skills.value[d[skill]];
ctx.beginPath();
ctx.moveTo(x * per + per / 2, y * per + per / 2);
ctx.lineTo(
...(s.loc.map(v => (v * 2 - 1) * per + per / 2) as LocArr)
);
if (skillTree.getSkillLevel(s.index) < v.front[i][1])
ctx.strokeStyle = '#aaa';
else if (skillTree.getSkillLevel(s.index) === s.max)
ctx.strokeStyle = '#ff0';
else ctx.strokeStyle = '#0f8';
ctx.lineWidth = devicePixelRatio;
ctx.stroke();
});
});
skills.value.forEach(v => {
const [x, y] = v.loc.map(v => v * 2 - 1);
const level = skillTree.getSkillLevel(v.index);
//
ctx.save();
ctx.lineWidth = per * 0.06;
if (selected.value === v.index) {
ctx.strokeStyle = '#ff0';
ctx.lineWidth *= 2;
} else if (level === 0) ctx.strokeStyle = '#888';
else if (level === v.max) ctx.strokeStyle = '#F7FF68';
else ctx.strokeStyle = '#00FF69';
ctx.strokeRect(x * per, y * per, per, per);
const img =
core.material.images.images[`skill${v.index}.png` as ImageIds];
ctx.drawImage(img, x * per, y * per, per, per);
if (selected.value === v.index) {
ctx.fillStyle = '#ff04';
ctx.fillRect(x * per, y * per, per, per);
}
ctx.restore();
});
}
function click(e: MouseEvent) {
const px = e.offsetX;
const py = e.offsetY;
const w = canvas.width / devicePixelRatio;
const per = w / 11;
const x = Math.floor(px / per);
const y = Math.floor(py / per);
if (x % 2 !== 1 || y % 2 !== 1) return;
const sx = Math.floor(x / 2) + 1;
const sy = Math.floor(y / 2) + 1;
const skill = skills.value.find(v => v.loc[0] === sx && v.loc[1] === sy);
if (!skill) return;
if (selected.value !== skill.index) selected.value = skill.index;
else {
upgrade(skill.index);
}
}
function upgrade(index: number) {
const success = skillTree.upgradeSkill(index);
if (!success) tip('error', '升级失败!');
else {
tip('success', '升级成功!');
update.value = !update.value;
core.status.route.push(`skill:${selected.value}`);
}
}
gameKey.use(props.ui.symbol);
gameKey
.realize('exit', () => {
exit();
})
.realize('confirm', () => {
upgrade(selected.value);
})
.realize('skillTree', () => {
exit();
});
onMounted(async () => {
canvas = document.getElementById('skill-canvas') as HTMLCanvasElement;
ctx = canvas.getContext('2d')!;
requestAnimationFrame(() => {
resize();
requestAnimationFrame(() => {
draw();
});
});
await sleep(50);
canvas.addEventListener('click', click);
});
onUnmounted(() => {
gameKey.dispose(props.ui.symbol);
});
function selectChapter(delta: number) {
const now = chapterList.indexOf(chapter.value);
const to = now + delta;
if (!isNil(chapterList[to]) && flags.chapter > to) {
selected.value = s[chapterList[to]][0].index;
chapter.value = chapterList[to];
update.value = !update.value;
flags.skillTree = to;
draw();
}
}
</script>
<style lang="less" scoped>
#skill-tree {
width: 90vh;
height: 90vh;
font-family: 'normal';
font-size: 150%;
display: flex;
flex-direction: column;
user-select: none;
}
#skill-title {
width: 100%;
text-align: center;
font-size: 130%;
height: 5vh;
line-height: 1;
}
#tools {
height: 5vh;
font-size: 3.2vh;
}
#skill-info {
height: 24vh;
display: flex;
flex-direction: column;
justify-content: space-between;
}
#divider {
width: 100%;
margin: 1vh 0;
}
#divider-split {
margin: 1vh 0 0 0;
}
#divider-vertical {
height: 100%;
margin: 0;
}
#skill-bottom {
height: 53vh;
width: 100%;
display: flex;
flex-direction: row;
}
#skill-canvas {
height: 53vh;
width: 53vh;
}
#skill-effect {
display: flex;
flex-direction: column;
}
#skill-consume {
width: 100%;
text-align: center;
height: 4vh;
}
#skill-upgrade-info {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding-top: 1vh;
justify-content: space-between;
#skill-upgrade-up {
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
}
#skill-upgrade-bottom {
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
}
}
.upgrade-divider {
margin: 1vh 0;
border-color: #ddd4;
}
#front-scroll {
width: 100%;
max-height: 30vh;
}
#skill-front {
display: flex;
flex-direction: column;
align-items: center;
}
@media screen and (max-width: 600px) {
#skill-tree {
width: 100%;
height: 100%;
font-size: 100%;
padding: 5%;
}
#skill-title {
width: 100%;
font-size: 130%;
height: 5vw;
}
#divider-vertical {
height: auto;
}
#skill-bottom {
height: auto;
flex-direction: column;
align-items: center;
}
#skill-canvas {
height: 35vh;
width: 35vh;
}
#front-scroll {
height: 18vh;
}
}
</style>

View File

@ -1,670 +0,0 @@
<template>
<div id="start">
<div id="start-div">
<img id="background" :src="bg.src" />
<div id="start-main">
<div id="title">人类开天辟地</div>
<div id="settings">
<div
id="sound"
class="setting-buttons"
:checked="soundChecked"
@click="bgm"
>
<sound-outlined />
<span v-if="!soundChecked" id="sound-del"></span>
</div>
<fullscreen-outlined
v-if="!fullscreen"
class="button-text setting-buttons2"
@click="setFullscreen"
/>
<fullscreen-exit-outlined
v-else
class="button-text setting-buttons2"
@click="setFullscreen"
/>
</div>
<div id="background-gradient"></div>
<div id="buttons">
<right-outlined id="cursor" />
<TransitionGroup name="start">
<span
class="start-button"
v-for="(v, i) of toshow"
:id="v"
:key="v"
:selected="selected === v"
:showed="showed"
:index="i"
:length="text[i].length"
@click="clickStartButton(v)"
@mouseenter="
movein(
$event.target as HTMLElement,
toshow.length - i - 1
)
"
>{{ text[i] }}</span
>
</TransitionGroup>
</div>
</div>
<div id="listen" @mousemove="onmove"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import {
RightOutlined,
SoundOutlined,
FullscreenOutlined,
FullscreenExitOutlined
} from '@ant-design/icons-vue';
import { sleep } from 'mutate-animate';
import { doByInterval, triggerFullscreen } from '../utils';
import { isMobile } from '../use';
import { gameKey } from '@motajs/system-action';
import { mainSetting } from '../preset/settingIns';
import { mat4 } from 'gl-matrix';
import { IMountedVBind } from '../interface';
const props = defineProps<IMountedVBind>();
const bg = core.material.images.images['bg.webp'];
let startdiv: HTMLDivElement;
let start: HTMLDivElement;
let main: HTMLDivElement;
let cursor: HTMLElement;
let background: HTMLImageElement;
let buttons: HTMLSpanElement[] = [];
let played: boolean = false;
const soundChecked = ref(false);
const fullscreen = ref(!!document.fullscreenElement);
const showed = ref(false);
const text1 = ['开始游戏', '读取存档', '录像回放', '查看成就'].reverse();
const text2 = ['轮回', '分支', '观测', '回忆'].reverse();
const ids = ['start-game', 'load-game', 'replay', 'achievement'].reverse();
const hardIds = ['easy', 'hard-hard', 'back'].reverse();
const hard = ['简单', '困难', '返回'].reverse();
const text = ref(text1);
const toshow = reactive<string[]>([]);
const selected = ref('start-game');
const perspective = mat4.create();
function resize() {
if (!window.core) return;
const scale = core.domStyle.scale;
const h = core._PY_;
const height = h * scale;
const width = height * 1.5;
if (!isMobile) {
startdiv.style.width = `${width}px`;
startdiv.style.height = `${height}px`;
main.style.fontSize = `${scale * 16}px`;
} else {
startdiv.style.width = `${window.innerWidth}px`;
startdiv.style.height = `${(window.innerHeight * 2) / 3}px`;
main.style.fontSize = `${scale * 8}px`;
}
}
function showCursor() {
cursor.style.opacity = '1';
setCursor(buttons[0], 0);
}
/**
* 设置光标位置
*/
function setCursor(ele: HTMLSpanElement, i: number) {
if (!ele) return;
const style = getComputedStyle(ele);
cursor.style.top = `${
parseFloat(style.height) * (i + 0.5) -
parseFloat(style.marginBottom) * (1 - i)
}px`;
cursor.style.left = `${
parseFloat(style.left) - 20 * core.domStyle.scale
}px`;
}
async function clickStartButton(id: string) {
if (id === 'start-game') showHard();
if (id === 'back') setButtonAnimate();
if (id === 'easy' || id === 'hard-hard') {
start.style.opacity = '0';
await sleep(600);
core.startGame(id === 'easy' ? 'easy' : 'hard');
}
if (id === 'load-game') {
start.style.top = '200vh';
core.load();
}
if (id === 'replay') core.chooseReplayFile();
if (id === 'achievement') {
props.controller.open('achievement');
}
}
function onmove(e: MouseEvent) {
if (!window.core) return;
const { offsetX, offsetY } = e;
const ele = e.target as HTMLDivElement;
const style = getComputedStyle(ele);
const width = parseFloat(style.width);
const height = parseFloat(style.height);
const cx = width / 2;
const cy = height / 2;
const dx = (offsetX - cx) / cx;
const dy = (offsetY - cy) / cy;
const matrix = mat4.identity(perspective);
mat4.scale(matrix, matrix, [1.2, 1.2, 1]);
mat4.rotateX(matrix, matrix, -(dy * 10 * Math.PI) / 180);
mat4.rotateY(matrix, matrix, (dx * 10 * Math.PI) / 180);
const end = Array.from(matrix).join(',');
background.style.transform = `perspective(${
1000 * core.domStyle.scale
}px)matrix3d(${end})`;
}
function movein(button: HTMLElement, i: number) {
setCursor(button, i);
selected.value = button.id;
}
gameKey.use(props.ui.symbol);
gameKey
.realize(
'@start_up',
() => {
const i = toshow.indexOf(selected.value);
const next = toshow[i + 1];
if (!next) return;
selected.value = next;
setCursor(buttons[toshow.length - i - 2], toshow.length - i - 2);
},
{ type: 'down-repeat' }
)
.realize(
'@start_down',
() => {
const i = toshow.indexOf(selected.value);
const next = toshow[i - 1];
if (!next) return;
selected.value = next;
setCursor(buttons[toshow.length - i], toshow.length - i);
},
{ type: 'down-repeat' }
)
.realize('confirm', () => {
clickStartButton(selected.value);
});
function bgm() {
// core.triggerBgm();
soundChecked.value = !soundChecked.value;
mainSetting.setValue('audio.bgmEnabled', soundChecked.value);
}
async function setFullscreen() {
const index = toshow.length - toshow.indexOf(selected.value) - 1;
await triggerFullscreen(!fullscreen.value);
requestAnimationFrame(() => {
fullscreen.value = !!document.fullscreenElement;
setCursor(buttons[index], index);
});
}
/**
* 初始 -> 难度
*/
async function showHard() {
cursor.style.transition =
'left 0.4s ease-out, top 0.4s ease-out, opacity 0.4s linear';
cursor.style.opacity = '0';
buttons.forEach(v => (v.style.transition = ''));
await doByInterval(
Array(4).fill(() => ids.unshift(toshow.pop()!)),
150
);
await sleep(250);
text.value = hard;
await doByInterval(
Array(3).fill(() => toshow.push(hardIds.shift()!)),
150
);
selected.value = 'easy';
nextTick(() => {
buttons = toshow
.map(v => document.getElementById(v) as HTMLSpanElement)
.reverse();
cursor.style.opacity = '1';
setCursor(buttons[0], 0);
});
await sleep(600);
buttons.forEach(
v =>
(v.style.transition =
'transform 0.3s ease-out, color 0.3s ease-out')
);
}
/**
* 难度 | -> 初始
*/
async function setButtonAnimate() {
if (toshow.length > 0) {
cursor.style.transition =
'left 0.4s ease-out, top 0.4s ease-out, opacity 0.4s linear';
cursor.style.opacity = '0';
buttons.forEach(v => (v.style.transition = ''));
await doByInterval(
Array(3).fill(() => hardIds.unshift(toshow.pop()!)),
150
);
}
text.value = text1;
if (played) {
text.value = text2;
}
await sleep(250);
await doByInterval(
Array(4).fill(() => toshow.push(ids.shift()!)),
150
);
selected.value = 'start-game';
nextTick(() => {
buttons = toshow
.map(v => document.getElementById(v) as HTMLSpanElement)
.reverse();
cursor.style.opacity = '1';
setCursor(buttons[0], 0);
buttons.forEach((v, i) => {});
});
if (!showed.value) await sleep(1200);
else await sleep(600);
buttons.forEach(
v =>
v &&
(v.style.transition =
'transform 0.3s ease-out, color 0.3s ease-out')
);
}
onMounted(async () => {
cursor = document.getElementById('cursor')!;
startdiv = document.getElementById('start-div') as HTMLDivElement;
main = document.getElementById('start-main') as HTMLDivElement;
start = document.getElementById('start') as HTMLDivElement;
background = document.getElementById('background') as HTMLImageElement;
window.addEventListener('resize', resize);
resize();
soundChecked.value = mainSetting.getValue('audio.bgmEnabled', true);
const { bgmController } = Mota.require('@user/client-modules');
bgmController.play('title.opus');
start.style.opacity = '1';
if (played) {
text.value = text2;
hard.splice(1, 0, '挑战');
}
setButtonAnimate().then(() => (showed.value = true));
await sleep(1000);
showCursor();
await sleep(1200);
});
onUnmounted(() => {
window.removeEventListener('resize', resize);
gameKey.dispose(props.ui.symbol);
});
</script>
<style lang="less" scoped>
#start {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.6s ease-out;
background-color: black;
object-fit: contain;
}
#start-div {
position: relative;
overflow: hidden;
object-fit: contain;
width: 100%;
height: 100%;
}
#background {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
filter: sepia(30%) contrast(115%);
transform: scale(120%);
}
#background-gradient {
z-index: 2;
position: absolute;
width: 200%;
height: 100%;
left: -100%;
background-image: linear-gradient(
45deg,
transparent 0%,
transparent 30%,
#000 60%,
#000 100%
);
animation: gradient 4s ease-out 0.5s 1 normal forwards;
pointer-events: none;
}
#listen {
position: absolute;
width: 100%;
height: 100%;
pointer-events: auto;
}
#start-main {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
font-size: 16px;
#title {
margin-top: 7%;
text-align: center;
font: 4em 'normal';
font-weight: 200;
background-image: linear-gradient(
to right,
rgb(0, 0, 0),
rgb(44, 44, 44),
rgb(136, 0, 214),
rgb(0, 2, 97),
rgb(0, 2, 97)
);
background-clip: text;
-webkit-background-clip: text;
text-shadow:
1px 1px 4px rgba(0, 0, 0, 0.5),
-1px -1px 3px rgba(255, 255, 255, 0.3),
5px 5px 5px rgba(0, 0, 0, 0.4);
filter: brightness(1.8);
user-select: none;
animation: opacity 3s ease-out 0.5s 1 normal forwards;
}
#buttons {
display: flex;
flex-direction: column-reverse;
justify-content: center;
position: absolute;
left: 18%;
bottom: 10%;
filter: brightness(120%) contrast(110%);
z-index: 1;
#cursor {
text-shadow: 2px 2px 3px black;
position: absolute;
opacity: 0;
animation: cursor 2.5s linear 0s infinite normal running;
transition:
left 0.4s ease-out,
top 0.4s ease-out,
opacity 1.5s ease-out;
}
.start-button {
position: relative;
font: bold 1.5em 'normal';
text-shadow:
1px 1px 2px rgba(0, 0, 0, 0.4),
0px 0px 1px rgba(255, 255, 255, 0.3);
background-clip: text;
-webkit-background-clip: text;
}
.start-button[index='1'][length='4'] {
left: 7.5%;
}
.start-button[index='2'][length='4'] {
left: 15%;
}
.start-button[index='3'][length='4'] {
left: 22.5%;
}
.start-button[index='1'][length='2'] {
left: 15%;
}
.start-button[index='2'][length='2'] {
left: 30%;
}
.start-button[index='3'][length='2'] {
left: 45%;
}
#start-game {
background-image: linear-gradient(
to bottom,
rgb(255, 255, 255),
rgb(0, 255, 255)
);
margin-bottom: 8%;
}
#load-game {
background-image: linear-gradient(
to bottom,
rgb(255, 255, 255),
rgb(0, 255, 55)
);
margin-bottom: 8%;
}
#replay {
background-image: linear-gradient(
to bottom,
rgb(255, 255, 255),
rgb(255, 251, 0)
);
margin-bottom: 8%;
}
#achievement {
background-image: linear-gradient(
to bottom,
rgb(255, 255, 255),
rgb(0, 208, 255)
);
margin-bottom: 8%;
}
#easy {
background-image: linear-gradient(
to bottom,
rgb(255, 255, 255),
rgb(87, 255, 72)
);
margin-bottom: 16%;
}
#hard-hard {
background-image: linear-gradient(
to bottom,
rgb(255, 255, 255),
rgb(255, 0, 0)
);
margin-bottom: 16%;
}
#back {
background-image: linear-gradient(
to bottom,
rgb(255, 255, 255),
rgb(132, 132, 132)
);
margin-bottom: 16%;
}
}
#settings {
position: absolute;
display: flex;
align-items: center;
flex-direction: row-reverse;
justify-content: flex-start;
right: 5%;
bottom: 10%;
font-size: 1.3em;
z-index: 1;
width: 50%;
.setting-buttons {
margin-left: 4%;
color: white;
transition: color 0.2s linear;
cursor: pointer;
}
.setting-buttons2 {
margin-left: 4%;
position: relative;
top: 1px;
}
#sound {
position: relative;
}
#sound[checked='false'] {
color: rgb(255, 43, 43);
}
#sound:hover {
color: aqua;
}
#sound[checked='false']:hover {
color: rgb(253, 139, 139);
}
#sound-del {
left: 0;
position: absolute;
width: 100%;
height: 100%;
border-bottom: 2px solid #aaa;
transform: translate(-85%, -50%) rotate(-45deg) scale(1.5);
}
}
}
.start-button {
cursor: pointer;
}
.start-button[selected='true'] {
color: transparent;
transform: scale(115%) translate(7.5%);
}
@keyframes cursor {
from {
transform: rotateX(0deg) scaleY(0.7);
}
to {
transform: rotateX(360deg) scaleY(0.7);
}
}
@keyframes gradient {
from {
left: -100%;
}
to {
left: 100%;
}
}
@keyframes opacity {
from {
color: #bbb;
}
to {
color: transparent;
}
}
.start-enter-active {
transition: all 1.2s ease-out;
}
.start-enter-active[showed='true'] {
transition: all 0.6s ease-out;
}
.start-enter-from {
opacity: 0;
transform: translateX(20px);
}
.start-leave-active {
transition: all 0.4s ease-out;
}
.start-leave-to {
transform: translateX(-20px);
opacity: 0;
}
@media screen and (max-width: 600px) {
#buttons {
font-size: 250%;
}
#start-main {
#title {
font-size: 700%;
}
#settings {
font-size: 400%;
}
}
}
</style>

View File

@ -1,46 +0,0 @@
<template>
<span ref="span" class="tip">{{ nowTip }}</span>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import tips from '../data/tips.json';
const span = ref<HTMLSpanElement>();
const nowTip = ref<string>();
function changeTip() {
const tip = tips[Math.floor(Math.random() * tips.length)];
if (!tip) return;
nowTip.value = '小贴士:' + tip;
}
const interval = window.setInterval(changeTip, 30_000);
function resize() {
const game = core.dom.gameDraw;
const right = window.innerWidth - (game.offsetLeft + game.offsetWidth);
const bottom = window.innerHeight - game.offsetTop;
if (!span.value) return;
span.value.style.right = `${right}px`;
span.value.style.bottom = `${bottom + 6}px`;
}
onMounted(() => {
window.addEventListener('resize', resize);
resize();
changeTip();
});
onUnmounted(() => {
window.removeEventListener('resize', resize);
window.clearInterval(interval);
});
</script>
<style lang="less" scoped>
.tip {
position: fixed;
font: 150% 'normal';
}
</style>

View File

@ -1,6 +1,6 @@
import { KeyCode } from '@motajs/client-base';
import { KeyboardEmits, Keyboard, isAssist } from '@motajs/system-action';
import { mainUi, fixedUi } from './preset/uiIns';
import { mainUi } from './preset/uiIns';
/**
*
@ -39,9 +39,3 @@ export function getVitualKeyOnce(
});
});
}
export function openDanmakuPoster() {
if (!fixedUi.hasName('danmakuEditor')) {
fixedUi.open('danmakuEditor');
}
}

View File

@ -11,7 +11,7 @@ main.floors.empty=
"images": [],
"ratio": 1,
"defaultGround": "ground",
"bgm": "bgm.mp3",
"bgm": "bgm.opus",
"firstArrive": [],
"eachArrive": [],
"parallelDo": "",

View File

@ -8,7 +8,7 @@ main.floors.sample0=
"canUseQuickShop": true,
"defaultGround": "ground",
"images": [],
"bgm": "bgm.mp3",
"bgm": "bgm.opus",
"ratio": 1,
"map": [
[ 0, 0,220, 0, 0, 20, 87, 3, 58, 59, 60, 61, 64],