mirror of
https://github.com/motajs/template.git
synced 2026-11-02 23:14:34 +08:00
refactor: UI 挪到 src
This commit is contained in:
1 parent
319d18d067
commit
8f32509199
35 files changed
+177
-370
No files matched your search
@@ -42,7 +42,7 @@ import {
|
||||
import { IRendererUsing, RendererUsing } from '@motajs/render-vue';
|
||||
import { IUIController, UIController } from '@motajs/system';
|
||||
import { createApp } from './render';
|
||||
import { LoadSceneUI } from './ui/load';
|
||||
import { LoadSceneUI } from '../../../src/client/ui/load';
|
||||
import { createUIPropsBase } from './ui/func';
|
||||
|
||||
export class ClientCore extends CoreState implements IClientCore {
|
||||
|
||||
@@ -10,6 +10,4 @@ export * from './ui';
|
||||
|
||||
export * from './client';
|
||||
export * from './common';
|
||||
export * from './core';
|
||||
export * from './shared';
|
||||
export * from './types';
|
||||
@@ -1,38 +0,0 @@
|
||||
import { gameKey } from '@motajs/system';
|
||||
import { POP_BOX_WIDTH, CENTER_LOC, FULL_LOC } from '../shared';
|
||||
import {
|
||||
saveSave,
|
||||
mainUIController,
|
||||
saveLoad,
|
||||
openSettings,
|
||||
openViewMap,
|
||||
openReplay,
|
||||
openStatistics
|
||||
} from '../ui';
|
||||
import { ElementLocator } from '@motajs/render';
|
||||
|
||||
export function createAction() {
|
||||
gameKey
|
||||
.realize('save', () => {
|
||||
saveSave(mainUIController, FULL_LOC);
|
||||
})
|
||||
.realize('statistics', () => {
|
||||
openStatistics(mainUIController);
|
||||
})
|
||||
.realize('load', () => {
|
||||
saveLoad(mainUIController, FULL_LOC);
|
||||
})
|
||||
.realize('menu', () => {
|
||||
const loc = CENTER_LOC.slice() as ElementLocator;
|
||||
loc[2] = POP_BOX_WIDTH;
|
||||
openSettings(mainUIController, loc);
|
||||
})
|
||||
.realize('replay', () => {
|
||||
const loc = CENTER_LOC.slice() as ElementLocator;
|
||||
loc[2] = POP_BOX_WIDTH;
|
||||
openReplay(mainUIController, loc);
|
||||
})
|
||||
.realize('viewMap', () => {
|
||||
openViewMap(mainUIController, FULL_LOC);
|
||||
});
|
||||
}
|
||||
@@ -1,38 +1,7 @@
|
||||
import { Font } from '@motajs/render';
|
||||
import { defineComponent } from 'vue';
|
||||
import { DEFAULT_FONT, MAIN_HEIGHT, MAIN_WIDTH } from '../shared';
|
||||
import { createElements } from '@user/client-base';
|
||||
import { mainRenderer } from './renderer';
|
||||
import { createUI } from '../ui';
|
||||
import { createAction } from './action';
|
||||
import { sceneController } from './scene';
|
||||
import { createWeather } from './weather';
|
||||
import { createApp } from './renderer';
|
||||
import { LoadSceneUI } from '../ui/load';
|
||||
|
||||
export function createGameRenderer() {
|
||||
const App = defineComponent(_props => {
|
||||
return () => (
|
||||
<container width={MAIN_WIDTH} height={MAIN_HEIGHT}>
|
||||
{sceneController.render()}
|
||||
</container>
|
||||
);
|
||||
});
|
||||
|
||||
mainRenderer.hide();
|
||||
createApp(App).mount(mainRenderer);
|
||||
|
||||
sceneController.open(LoadSceneUI, {});
|
||||
mainRenderer.show();
|
||||
}
|
||||
|
||||
export function createRender() {
|
||||
createElements();
|
||||
createUI();
|
||||
createAction();
|
||||
createWeather();
|
||||
|
||||
Font.setDefaults(DEFAULT_FONT);
|
||||
}
|
||||
|
||||
export * from './fx';
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
import { WeatherController } from './controller';
|
||||
import {
|
||||
CloudWeather,
|
||||
FogWeather,
|
||||
RainWeather,
|
||||
SnowWeather,
|
||||
SunWeather
|
||||
} from './presets';
|
||||
|
||||
export function createWeather() {
|
||||
WeatherController.register('cloud', CloudWeather);
|
||||
WeatherController.register('rain', RainWeather);
|
||||
WeatherController.register('snow', SnowWeather);
|
||||
WeatherController.register('sun', SunWeather);
|
||||
WeatherController.register('fog', FogWeather);
|
||||
}
|
||||
|
||||
export * from './presets';
|
||||
export * from './controller';
|
||||
export * from './types';
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import { ElementLocator, Font } from '@motajs/render';
|
||||
|
||||
// 本文件为 UI 配置文件,你可以修改下面的每个常量来控制 UI 的显示参数,每个常量都有注释说明
|
||||
|
||||
//#region 地图
|
||||
|
||||
/** 每个格子的默认宽度,现阶段用处不大 */
|
||||
export const CELL_WIDTH = 32;
|
||||
/** 每个格子的默认高度,现阶段用处不大 */
|
||||
export const CELL_HEIGHT = 32;
|
||||
/** 每个格子的宽高 */
|
||||
export const CELL_SIZE = 32;
|
||||
/** 地图格子宽度,此处仅影响画面,不影响游戏内逻辑,游戏内逻辑地图大小请在 core.js 中修改 */
|
||||
export const MAP_BLOCK_WIDTH = 13;
|
||||
/** 地图格子高度,此处仅影响画面,不影响游戏内逻辑,游戏内逻辑地图大小请在 core.js 中修改 */
|
||||
export const MAP_BLOCK_HEIGHT = 13;
|
||||
/** 地图像素宽度 */
|
||||
export const MAP_WIDTH = CELL_SIZE * MAP_BLOCK_WIDTH;
|
||||
/** 地图像素高度 */
|
||||
export const MAP_HEIGHT = CELL_SIZE * MAP_BLOCK_HEIGHT;
|
||||
/** 地图宽度的一半 */
|
||||
export const HALF_MAP_WIDTH = MAP_WIDTH / 2;
|
||||
/** 地图高度的一半 */
|
||||
export const HALF_MAP_HEIGHT = MAP_HEIGHT / 2;
|
||||
/**
|
||||
* 动态内容预留,不明白含义的话不要动。地图上所有正在移动的图块称为动态内容,这些内容的数量无法预测,因此需要预留数组大小,
|
||||
* 如果不够再临时扩充。如果你的塔中有大量的移动操作,可以适当提高此值,避免频繁的内存扩充行为,可以一定程度上提高性能表现。
|
||||
*/
|
||||
export const DYNAMIC_RESERVE = 16;
|
||||
/**
|
||||
* 移动图块容忍度,不明白含义的话不要动。如果移动图块的数量长期小于当前预留数量,那么将会降低预留数量,提升性能表现。
|
||||
* 调整此值可以调整频率,值越大,越不容易因为数量小于预留数量而减小预留。
|
||||
*/
|
||||
export const MOVING_TOLERANCE = 60;
|
||||
/** 开关门动画的动画时长 */
|
||||
export const DOOR_ANIMATE_INTERVAL = 50;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 状态栏
|
||||
|
||||
/** 状态栏像素宽度 */
|
||||
export const STATUS_BAR_WIDTH = 180;
|
||||
/** 状态栏像素高度 */
|
||||
export const STATUS_BAR_HEIGHT = 32 * MAP_BLOCK_HEIGHT;
|
||||
/** 右侧状态栏的横坐标 */
|
||||
export const RIGHT_STATUS_POS = STATUS_BAR_WIDTH + MAP_WIDTH;
|
||||
/** 是否启用右侧状态栏 */
|
||||
export const ENABLE_RIGHT_STATUS_BAR = false;
|
||||
/** 状态栏数量,启用右侧状态栏为两个,不启用为一个 */
|
||||
export const STATUS_BAR_COUNT = ENABLE_RIGHT_STATUS_BAR ? 2 : 1;
|
||||
/** 状态栏宽度的一半 */
|
||||
export const HALF_STATUS_WIDTH = STATUS_BAR_WIDTH / 2;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 游戏画面
|
||||
|
||||
/** 游戏画面像素宽度,宽=地图宽度+状态栏宽度*状态栏数量 */
|
||||
export const MAIN_WIDTH = MAP_WIDTH + STATUS_BAR_WIDTH * STATUS_BAR_COUNT;
|
||||
/** 游戏画面像素高度 */
|
||||
export const MAIN_HEIGHT = MAP_HEIGHT;
|
||||
/** 游戏画面宽度的一半 */
|
||||
export const HALF_WIDTH = MAIN_WIDTH / 2;
|
||||
/** 游戏画面高度的一半 */
|
||||
export const HALF_HEIGHT = MAIN_HEIGHT / 2;
|
||||
/** 全屏显示的 loc */
|
||||
export const FULL_LOC: ElementLocator = [0, 0, MAIN_WIDTH, MAIN_HEIGHT];
|
||||
/** 居中显示的 loc */
|
||||
export const CENTER_LOC: ElementLocator = [
|
||||
HALF_WIDTH,
|
||||
HALF_HEIGHT,
|
||||
void 0,
|
||||
void 0,
|
||||
0.5,
|
||||
0.5
|
||||
];
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 通用配置
|
||||
|
||||
/** 弹框的宽度,使用在内置 UI 与组件中,包括确认框、选择框、等待框等 */
|
||||
export const POP_BOX_WIDTH = MAP_WIDTH / 2;
|
||||
/** 默认字体 */
|
||||
export const DEFAULT_FONT = new Font('Verdana', 16);
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 加载界面
|
||||
|
||||
/** 加载界面的任务进度条半径 */
|
||||
export const LOAD_TASK_RADIUS = Math.min(MAIN_WIDTH, MAIN_HEIGHT) / 6;
|
||||
/** 加载界面的字节进度条纵轴位置 */
|
||||
export const LOAD_BYTE_HEIGHT = MAIN_HEIGHT / 2 + MAIN_HEIGHT / 4;
|
||||
/** 加载界面任务进度条的纵轴位置 */
|
||||
export const LOAD_TASK_CENTER_HEIGHT = MAIN_HEIGHT / 2 - MAIN_HEIGHT / 8;
|
||||
/** 加载界面字节进度条的长度 */
|
||||
export const LOAD_BYTE_LENGTH = MAIN_WIDTH - MAIN_WIDTH / 12;
|
||||
/** 加载界面任务进度条的粗细 */
|
||||
export const LOAD_TASK_LINE_WIDTH = 6;
|
||||
/** 加载界面字节进度条的粗细 */
|
||||
export const LOAD_BYTE_LINE_WIDTH = 6;
|
||||
/** 已加载部分进度条的颜色 */
|
||||
export const LOAD_LOADED_COLOR = '#57ff78';
|
||||
/** 未加载部分进度条的颜色 */
|
||||
export const LOAD_UNLOADED_COLOR = '#ccc';
|
||||
/** 加载界面的文字颜色 */
|
||||
export const LOAD_FONT_COLOR = '#fff';
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 存档界面
|
||||
|
||||
/** 存档缩略图尺寸 */
|
||||
export const SAVE_ITEM_SIZE = MAP_BLOCK_WIDTH * 10;
|
||||
/** 单个存档上方显示第几号存档的高度 */
|
||||
export const SAVE_ITEM_TOP = 24;
|
||||
/** 单个存档下方显示这个存档信息的高度 */
|
||||
export const SAVE_ITEM_DOWN = 24;
|
||||
/** 单个存档高度,包括存档下方的信息 */
|
||||
export const SAVE_ITEM_HEIGHT = SAVE_ITEM_SIZE + SAVE_ITEM_TOP + SAVE_ITEM_DOWN;
|
||||
/** 存档间距 */
|
||||
export const SAVE_INTERVAL = 20;
|
||||
/** 存档下巴高度,即下方显示页码和返回按钮的高度 */
|
||||
export const SAVE_DOWN_PAD = 30;
|
||||
/** 存档页码数,调高并不会影响性能,但是如果玩家存档太多的话会导致存档体积很大 */
|
||||
export const SAVE_PAGES = 1000;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 标题界面
|
||||
|
||||
/** 标题图 */
|
||||
export const TITLE_BACKGROUND_IMAGE = 'bg.jpg';
|
||||
|
||||
/** 标题文字中心横坐标 */
|
||||
export const TITLE_X = HALF_WIDTH;
|
||||
/** 标题文字中心纵坐标 */
|
||||
export const TITLE_Y = 100;
|
||||
/** 标题文字的填充颜色 */
|
||||
export const TITLE_FILL = 'white';
|
||||
/** 标题文字的描边颜色 */
|
||||
export const TITLE_STROKE = 'black';
|
||||
/** 标题文字的描边宽度 */
|
||||
export const TITLE_STROKE_WIDTH = 2;
|
||||
|
||||
/** 标题界面按钮宽度,如果文字被裁剪可以考虑扩大此值 */
|
||||
export const BUTTONS_WIDTH = 160;
|
||||
/** 标题界面按钮高度,如果文字被裁剪可以考虑扩大此值 */
|
||||
export const BUTTONS_HEIGHT = 200;
|
||||
/** 标题界面按钮中心横坐标 */
|
||||
export const BUTTONS_X = HALF_WIDTH;
|
||||
/** 标题界面按钮左上角纵坐标 */
|
||||
export const BUTTONS_Y = MAIN_HEIGHT - BUTTONS_HEIGHT;
|
||||
|
||||
//#endregion
|
||||
@@ -77,3 +77,8 @@ export interface IClientCore extends IClientSystem {
|
||||
id: string
|
||||
): Readonly<IClientPlugin<T>> | null;
|
||||
}
|
||||
|
||||
export interface IClientCoreExtended {
|
||||
/** 渲染端主对象 */
|
||||
readonly state: IClientCore;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import {
|
||||
GameUI,
|
||||
SetupComponentOptions,
|
||||
IUIDefaultPropsBase
|
||||
} from '@motajs/system';
|
||||
import { DefaultProps } from '@motajs/render-vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { MAIN_WIDTH, MAIN_HEIGHT } from '../shared';
|
||||
|
||||
export interface MainBackgroundProps
|
||||
extends DefaultProps, IUIDefaultPropsBase {}
|
||||
|
||||
const mainBackgroundProps = {
|
||||
props: ['controller', 'instance']
|
||||
} satisfies SetupComponentOptions<MainBackgroundProps>;
|
||||
|
||||
export const MainBackground = defineComponent<MainBackgroundProps>(() => {
|
||||
return () => (
|
||||
<g-rect
|
||||
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
|
||||
fill
|
||||
fillStyle="rgba(0, 0, 0, 0.8)"
|
||||
/>
|
||||
);
|
||||
}, mainBackgroundProps);
|
||||
|
||||
/** 主 UI 控制器的背景 UI */
|
||||
export const MainBackgroundUI = new GameUI('main-background', MainBackground);
|
||||
@@ -1,251 +0,0 @@
|
||||
import { GameUI, SetupComponentOptions } from '@motajs/system';
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { Font } from '@motajs/render';
|
||||
import { ITileLocator } from '@motajs/common';
|
||||
import { Scroll } from '@user/client-base';
|
||||
import { IEnemyView } from '@user/data-system';
|
||||
import { IReadonlyEnemy } from '@user/data-base';
|
||||
import { IEnemyAttr } from '@user/data-common';
|
||||
import { BookDetailUI } from './bookDetail';
|
||||
import { FULL_LOC, MAIN_HEIGHT, MAIN_WIDTH } from '../shared';
|
||||
import { IUIPropsBase } from './types';
|
||||
|
||||
export interface BookProps extends IUIPropsBase {}
|
||||
|
||||
/** 怪物手册组件的 props 声明 */
|
||||
const bookProps = {
|
||||
props: ['controller', 'instance', 'state']
|
||||
} satisfies SetupComponentOptions<BookProps>;
|
||||
|
||||
/** 列表中每一行的高度 */
|
||||
const ROW_HEIGHT = 72;
|
||||
/** 列表区域的起始纵坐标 */
|
||||
const LIST_TOP = 44;
|
||||
/** 列表区域的横坐标 */
|
||||
const LIST_LEFT = 16;
|
||||
/** 列表区域的宽度 */
|
||||
const LIST_WIDTH = MAIN_WIDTH - 32;
|
||||
/** 列表区域的高度,底部留出退出按钮的空间 */
|
||||
const LIST_HEIGHT = MAIN_HEIGHT - LIST_TOP - 36;
|
||||
/** 数值列区域的起始横坐标 */
|
||||
const STAT_LEFT = 240;
|
||||
|
||||
/** 怪物名称字体 */
|
||||
const nameFont = Font.defaults({ size: 15, weight: 700 });
|
||||
/** 特殊名与属性字体 */
|
||||
const statFont = Font.defaults({ size: 12 });
|
||||
|
||||
/** 怪物手册列表组件 */
|
||||
export const Book = defineComponent<BookProps>(props => {
|
||||
/** 当前地图的怪物上下文 */
|
||||
const ctx = props.state.enemyContext;
|
||||
|
||||
// 按怪物图块数字去重,每个数字保留一个代表视图
|
||||
const enemies = computed(() => {
|
||||
const map = new Map<number, IEnemyView<IEnemyAttr>>();
|
||||
for (const pair of ctx.iterateEnemy()) {
|
||||
const base = pair[1].getBaseEnemy();
|
||||
if (!map.has(base.code)) map.set(base.code, pair[1]);
|
||||
}
|
||||
return Array.from(map.values());
|
||||
});
|
||||
|
||||
/** 将数值按大数规则格式化为展示字符串 */
|
||||
const format = (value: number) => {
|
||||
return core.formatBigNumber(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 以基础属性路径计算怪物临界信息,已达临界上界或未绑定伤害系统时返回 null
|
||||
*/
|
||||
const getCritical = (
|
||||
base: IReadonlyEnemy<IEnemyAttr>,
|
||||
locator: ITileLocator | null
|
||||
) => {
|
||||
const damage = ctx.getDamageSystem();
|
||||
// TODO: 伤害系统未绑定或已达临界上界时,伤害 / 临界 / 减伤 / 「1 防」留空
|
||||
if (!damage) return null;
|
||||
const first = damage
|
||||
.calculateEnemyCritical(base, 'atk', locator)
|
||||
.next();
|
||||
if (first.done) return null;
|
||||
return first.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 以基础属性路径计算「1 防」时的伤害信息,即隔离属性增加 1 点防御后的战斗信息
|
||||
*/
|
||||
const getDef1 = (
|
||||
base: IReadonlyEnemy<IEnemyAttr>,
|
||||
locator: ITileLocator | null
|
||||
) => {
|
||||
const damage = ctx.getDamageSystem();
|
||||
if (!damage) return null;
|
||||
const isolated = props.state.hero.getIsolatedAttribute();
|
||||
isolated.add('def', 1);
|
||||
const first = damage
|
||||
.with(isolated)
|
||||
.calculateEnemyCritical(base, 'atk', locator)
|
||||
.next();
|
||||
if (first.done) return null;
|
||||
return first.value;
|
||||
};
|
||||
|
||||
/** 拼接怪物全部特殊属性的名称,以空格分隔 */
|
||||
const specialText = (base: IReadonlyEnemy<IEnemyAttr>) => {
|
||||
const names: string[] = [];
|
||||
for (const special of base.iterateSpecials()) {
|
||||
names.push(special.getSpecialName());
|
||||
}
|
||||
return names.join(' ');
|
||||
};
|
||||
|
||||
/** 计算第 index 个数值列的中心横坐标 */
|
||||
const statX = (index: number) => {
|
||||
return STAT_LEFT + (index * (MAIN_WIDTH - STAT_LEFT)) / 5;
|
||||
};
|
||||
|
||||
/** 打开指定怪物视图的详细信息界面 */
|
||||
const openDetail = (view: IEnemyView<IEnemyAttr>) => {
|
||||
props.controller.open(BookDetailUI, { enemy: view });
|
||||
};
|
||||
|
||||
/** 关闭当前怪物手册界面 */
|
||||
const exit = () => props.controller.close(props.instance);
|
||||
|
||||
return () => (
|
||||
<container loc={FULL_LOC}>
|
||||
{/* 标题 */}
|
||||
<text
|
||||
text="怪物手册"
|
||||
loc={[24, 22]}
|
||||
anc={[0, 0]}
|
||||
font={nameFont}
|
||||
fillStyle="gold"
|
||||
/>
|
||||
{/* 怪物列表区 */}
|
||||
<Scroll
|
||||
noscroll
|
||||
loc={[LIST_LEFT, LIST_TOP, LIST_WIDTH, LIST_HEIGHT]}
|
||||
>
|
||||
{enemies.value.map((view, i) => {
|
||||
const base = view.getBaseEnemy();
|
||||
const locator = ctx.getEnemyLocatorByView(view);
|
||||
const critical = getCritical(base, locator);
|
||||
const def1 = getDef1(base, locator);
|
||||
const y = i * ROW_HEIGHT;
|
||||
return (
|
||||
<container
|
||||
key={base.code}
|
||||
loc={[0, y, LIST_WIDTH, ROW_HEIGHT]}
|
||||
cursor="pointer"
|
||||
onClick={() => openDetail(view)}
|
||||
>
|
||||
<icon icon={base.code} loc={[8, 16, 40, 40]} />
|
||||
<text
|
||||
text={base.getAttribute('name')}
|
||||
loc={[56, 14]}
|
||||
anc={[0, 0]}
|
||||
font={nameFont}
|
||||
fillStyle="#ffffff"
|
||||
/>
|
||||
<text
|
||||
text={specialText(base)}
|
||||
loc={[56, 36]}
|
||||
anc={[0, 0]}
|
||||
font={statFont}
|
||||
fillStyle="#ffd27f"
|
||||
/>
|
||||
{/* 数值列表区 */}
|
||||
<text
|
||||
text={`生命 ${format(base.getAttribute('hp'))}`}
|
||||
loc={[statX(0), 22]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightgreen"
|
||||
/>
|
||||
<text
|
||||
text={`攻击 ${format(base.getAttribute('atk'))}`}
|
||||
loc={[statX(1), 22]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightcoral"
|
||||
/>
|
||||
<text
|
||||
text={`防御 ${format(base.getAttribute('def'))}`}
|
||||
loc={[statX(2), 22]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightblue"
|
||||
/>
|
||||
<text
|
||||
text={`金币 ${format(base.getAttribute('money'))}`}
|
||||
loc={[statX(3), 22]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightyellow"
|
||||
/>
|
||||
<text
|
||||
text={`经验 ${format(base.getAttribute('exp'))}`}
|
||||
loc={[statX(4), 22]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lawngreen"
|
||||
/>
|
||||
{/* 临界信息区 */}
|
||||
{critical && (
|
||||
<text
|
||||
text={`伤害 ${format(critical.baseInfo.damage)}`}
|
||||
loc={[statX(0), 52]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="#ffffff"
|
||||
/>
|
||||
)}
|
||||
{critical && (
|
||||
<text
|
||||
text={`临界 ${format(critical.nextDiff)}`}
|
||||
loc={[statX(1), 52]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightsalmon"
|
||||
/>
|
||||
)}
|
||||
{critical && (
|
||||
<text
|
||||
text={`减伤 ${format(-critical.damageDiff)}`}
|
||||
loc={[statX(2), 52]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightpink"
|
||||
/>
|
||||
)}
|
||||
{critical && def1 && (
|
||||
<text
|
||||
text={`1 防 ${format(critical.baseInfo.damage - def1.baseInfo.damage)}`}
|
||||
loc={[statX(3), 52]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="cyan"
|
||||
/>
|
||||
)}
|
||||
</container>
|
||||
);
|
||||
})}
|
||||
</Scroll>
|
||||
{/* 右下角退出按钮 */}
|
||||
<text
|
||||
text="退出"
|
||||
loc={[MAIN_WIDTH - 16, MAIN_HEIGHT - 16]}
|
||||
anc={[1, 1]}
|
||||
font={nameFont}
|
||||
cursor="pointer"
|
||||
fillStyle="#ffffff"
|
||||
onClick={exit}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
}, bookProps);
|
||||
|
||||
/** 怪物手册列表的 UI 控制器 */
|
||||
export const BookUI = new GameUI('book', Book);
|
||||
@@ -1,247 +0,0 @@
|
||||
import { GameUI, SetupComponentOptions } from '@motajs/system';
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import { Font } from '@motajs/render';
|
||||
import { ITileLocator } from '@motajs/common';
|
||||
import { Scroll, TextContent } from '@user/client-base';
|
||||
import { IEnemyCritical, IEnemyView, EnemyView } from '@user/data-system';
|
||||
import { IEnemy, IReadonlyEnemy } from '@user/data-base';
|
||||
import { IEnemyAttr } from '@user/data-common';
|
||||
import { FULL_LOC, MAIN_HEIGHT, MAIN_WIDTH } from '../shared';
|
||||
import { IUIPropsBase } from './types';
|
||||
|
||||
/** 详情当前展示的属性模式 */
|
||||
const enum DetailMode {
|
||||
/** 基础属性,即不考虑光环等加成的原始状态 */
|
||||
Base,
|
||||
/** 计算后属性,即含光环等加成的状态 */
|
||||
Computed
|
||||
}
|
||||
|
||||
export interface BookDetailProps extends IUIPropsBase {
|
||||
enemy: IEnemy<IEnemyAttr> | IEnemyView<IEnemyAttr>;
|
||||
}
|
||||
|
||||
/** 怪物详情组件的 props 声明 */
|
||||
const bookDetailProps = {
|
||||
props: ['controller', 'instance', 'state', 'enemy']
|
||||
} satisfies SetupComponentOptions<BookDetailProps>;
|
||||
|
||||
/** 怪物名称字体 */
|
||||
const nameFont = Font.defaults({ size: 20, weight: 700 });
|
||||
/** 属性数值字体 */
|
||||
const statFont = Font.defaults({ size: 15 });
|
||||
/** 特殊属性字体 */
|
||||
const specialFont = Font.defaults({ size: 14 });
|
||||
|
||||
/** 怪物详细信息组件 */
|
||||
export const BookDetail = defineComponent<BookDetailProps>(props => {
|
||||
/** 当前展示的属性模式 */
|
||||
const mode = ref(DetailMode.Base);
|
||||
|
||||
/** 若入参为怪物视图则返回该视图,否则返回 null */
|
||||
const getView = (): IEnemyView<IEnemyAttr> | null => {
|
||||
if (props.enemy instanceof EnemyView) return props.enemy;
|
||||
return null;
|
||||
};
|
||||
|
||||
/** 获取基础属性,视图取基础怪物,否则取入参本身 */
|
||||
const getBase = (): IReadonlyEnemy<IEnemyAttr> => {
|
||||
if (props.enemy instanceof EnemyView) return props.enemy.getBaseEnemy();
|
||||
else return props.enemy as IReadonlyEnemy<IEnemyAttr>;
|
||||
};
|
||||
|
||||
/** 获取计算后属性,视图走计算接口,否则取入参本身 */
|
||||
const getComputed = (): IReadonlyEnemy<IEnemyAttr> => {
|
||||
if (props.enemy instanceof EnemyView) {
|
||||
return props.enemy.getComputedEnemy();
|
||||
} else {
|
||||
return props.enemy as IReadonlyEnemy<IEnemyAttr>;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: 后续支持「在地图中打开指定 View 详情」时,locator 需按实际位置补全
|
||||
const getLocator = (): ITileLocator | null => {
|
||||
const view = getView();
|
||||
if (!view) return null;
|
||||
return view.context.getEnemyLocatorByView(view);
|
||||
};
|
||||
|
||||
/** 当前模式对应的怪物数据 */
|
||||
const current = computed(() =>
|
||||
mode.value === DetailMode.Base ? getBase() : getComputed()
|
||||
);
|
||||
|
||||
// 详情仅展示特殊属性列表,不展示光环
|
||||
const specials = computed(() =>
|
||||
Array.from(current.value.iterateSpecials())
|
||||
);
|
||||
|
||||
/**
|
||||
* 计算当前模式下的临界信息,基础模式走裸怪物临界接口,
|
||||
* 计算模式走怪物视图临界接口,已达上界时返回 null
|
||||
*/
|
||||
const critical = computed<IEnemyCritical | null>(() => {
|
||||
const damage = props.state.enemyContext.getDamageSystem();
|
||||
if (!damage) return null;
|
||||
if (mode.value === DetailMode.Base) {
|
||||
const first = damage
|
||||
.calculateEnemyCritical(getBase(), 'atk', getLocator())
|
||||
.next();
|
||||
return first.done ? null : first.value;
|
||||
}
|
||||
const view = getView();
|
||||
if (!view) return null;
|
||||
const first = damage.calculateViewCritical(view, 'atk').next();
|
||||
return first.done ? null : first.value;
|
||||
});
|
||||
|
||||
/** 将数值按大数规则格式化为展示字符串 */
|
||||
const format = (value: number) => core.formatBigNumber(value);
|
||||
|
||||
/** 计算第 index 个数值列的中心横坐标 */
|
||||
const statX = (index: number) => 24 + (index * (MAIN_WIDTH - 48)) / 5;
|
||||
|
||||
/** 在基础属性与计算后属性之间切换 */
|
||||
const toggleMode = () => {
|
||||
if (!(props.enemy instanceof EnemyView)) return;
|
||||
mode.value =
|
||||
mode.value === DetailMode.Base
|
||||
? DetailMode.Computed
|
||||
: DetailMode.Base;
|
||||
};
|
||||
|
||||
/** 关闭当前怪物详情界面 */
|
||||
const close = () => props.controller.close(props.instance);
|
||||
|
||||
/** 左下角切换按钮的显示文本 */
|
||||
const toggleText = computed(() =>
|
||||
mode.value === DetailMode.Base ? '切换至计算后属性' : '切换至基础属性'
|
||||
);
|
||||
|
||||
return () => {
|
||||
const enemy = current.value;
|
||||
return (
|
||||
<container loc={FULL_LOC}>
|
||||
{/* 标题 */}
|
||||
<text
|
||||
text={enemy.getAttribute('name')}
|
||||
loc={[24, 22]}
|
||||
anc={[0, 0]}
|
||||
font={nameFont}
|
||||
fillStyle="gold"
|
||||
/>
|
||||
{/* 属性数值区 */}
|
||||
<text
|
||||
text={`生命 ${format(enemy.getAttribute('hp'))}`}
|
||||
loc={[statX(0), 64]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightgreen"
|
||||
/>
|
||||
<text
|
||||
text={`攻击 ${format(enemy.getAttribute('atk'))}`}
|
||||
loc={[statX(1), 64]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightcoral"
|
||||
/>
|
||||
<text
|
||||
text={`防御 ${format(enemy.getAttribute('def'))}`}
|
||||
loc={[statX(2), 64]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightblue"
|
||||
/>
|
||||
<text
|
||||
text={`金币 ${format(enemy.getAttribute('money'))}`}
|
||||
loc={[statX(3), 64]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightyellow"
|
||||
/>
|
||||
<text
|
||||
text={`经验 ${format(enemy.getAttribute('exp'))}`}
|
||||
loc={[statX(4), 64]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lawngreen"
|
||||
/>
|
||||
{/* 特殊属性列表区 */}
|
||||
<Scroll noscroll loc={[24, 92, MAIN_WIDTH - 48, 150]}>
|
||||
{specials.value.map((special, i) => {
|
||||
const y = i * 44;
|
||||
return (
|
||||
<container
|
||||
key={special.code}
|
||||
loc={[0, y, MAIN_WIDTH - 48, 44]}
|
||||
>
|
||||
<text
|
||||
text={`${special.getSpecialName()}:`}
|
||||
loc={[0, 6]}
|
||||
anc={[0, 0]}
|
||||
font={specialFont}
|
||||
fillStyle={special.getNameColor()}
|
||||
/>
|
||||
<TextContent
|
||||
text={special.getDescription()}
|
||||
width={MAIN_WIDTH - 48}
|
||||
loc={[0, 24, MAIN_WIDTH - 48, 20]}
|
||||
font={specialFont}
|
||||
fillStyle="#dddddd"
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
})}
|
||||
</Scroll>
|
||||
<text
|
||||
text="临界"
|
||||
loc={[24, 258]}
|
||||
anc={[0, 0.5]}
|
||||
font={specialFont}
|
||||
fillStyle="#fffa9e"
|
||||
/>
|
||||
{/* 临界信息区 */}
|
||||
{critical.value && (
|
||||
<text
|
||||
text={`加攻 ${format(critical.value.nextDiff)}`}
|
||||
loc={[24, 282]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightsalmon"
|
||||
/>
|
||||
)}
|
||||
{critical.value && (
|
||||
<text
|
||||
text={`减伤 ${format(-critical.value.damageDiff)}`}
|
||||
loc={[160, 282]}
|
||||
anc={[0, 0.5]}
|
||||
font={statFont}
|
||||
fillStyle="lightpink"
|
||||
/>
|
||||
)}
|
||||
{/* 左下切换按钮 / 右下返回按钮 */}
|
||||
<text
|
||||
text={toggleText.value}
|
||||
loc={[24, MAIN_HEIGHT - 18]}
|
||||
anc={[0, 1]}
|
||||
font={statFont}
|
||||
cursor="pointer"
|
||||
fillStyle="gold"
|
||||
onClick={toggleMode}
|
||||
/>
|
||||
<text
|
||||
text="返回上一级"
|
||||
loc={[MAIN_WIDTH - 24, MAIN_HEIGHT - 18]}
|
||||
anc={[1, 1]}
|
||||
font={statFont}
|
||||
cursor="pointer"
|
||||
fillStyle="#ffffff"
|
||||
onClick={close}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
};
|
||||
}, bookDetailProps);
|
||||
|
||||
/** 怪物详细信息的 UI 控制器 */
|
||||
export const BookDetailUI = new GameUI('book-detail', BookDetail);
|
||||
@@ -1,551 +0,0 @@
|
||||
import { GameUI, SetupComponentOptions } from '@motajs/system';
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { Font } from '@motajs/render';
|
||||
import { Scroll, TextContent } from '@user/client-base';
|
||||
import { EquipStatus, IHeroState } from '@user/data-base';
|
||||
import { IHeroAttr } from '@user/data-common';
|
||||
import { FULL_LOC, MAIN_HEIGHT, MAIN_WIDTH } from '../shared';
|
||||
import { IUIPropsBase } from './types';
|
||||
|
||||
interface IHeroAttrItem {
|
||||
/** 勇士属性的键名 */
|
||||
readonly id: keyof IHeroAttr;
|
||||
/** 该属性在状态区域显示的中文名 */
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface EquipBoxProps extends IUIPropsBase {
|
||||
/** 勇士状态对象 */
|
||||
heroState: IHeroState<IHeroAttr>;
|
||||
/** 状态区域要列举的属性,按数组顺序自上而下展示 */
|
||||
attributes: readonly IHeroAttrItem[];
|
||||
}
|
||||
|
||||
/** 装备界面组件的 props 声明 */
|
||||
const equipBoxProps = {
|
||||
props: ['controller', 'instance', 'state', 'heroState', 'attributes']
|
||||
} satisfies SetupComponentOptions<EquipBoxProps>;
|
||||
|
||||
/** 左侧状态区域宽度 */
|
||||
const STATUS_WIDTH = 180;
|
||||
/** 左侧状态区域标题占用的高度 */
|
||||
const STATUS_HEADER = 28;
|
||||
/** 左侧状态区域每一行的高度 */
|
||||
const STATUS_ROW_HEIGHT = 26;
|
||||
/** 右侧区域起始横坐标 */
|
||||
const RIGHT_LEFT = STATUS_WIDTH;
|
||||
/** 右侧区域宽度 */
|
||||
const RIGHT_WIDTH = MAIN_WIDTH - RIGHT_LEFT;
|
||||
/** 装备描述区域顶部纵坐标 */
|
||||
const DESC_TOP = 8;
|
||||
/** 装备描述区域高度 */
|
||||
const DESC_HEIGHT = 100;
|
||||
/** 装备槽区域顶部纵坐标 */
|
||||
const SLOT_TOP = DESC_TOP + DESC_HEIGHT;
|
||||
/** 装备槽区域高度 */
|
||||
const SLOT_HEIGHT = 112;
|
||||
/** 单个装备槽的宽度 */
|
||||
const SLOT_CELL_WIDTH = 76;
|
||||
/** 装备槽内图标的尺寸 */
|
||||
const SLOT_ICON_SIZE = 40;
|
||||
/** 拥有装备区域顶部纵坐标 */
|
||||
const OWNED_TOP = SLOT_TOP + SLOT_HEIGHT;
|
||||
/** 拥有装备区域高度,底部为退出按钮留出空间 */
|
||||
const OWNED_HEIGHT = MAIN_HEIGHT - OWNED_TOP - 32;
|
||||
/** 拥有装备区域标题占用的高度 */
|
||||
const OWNED_HEADER = 22;
|
||||
/** 拥有装备每一行的高度 */
|
||||
const OWNED_ROW_HEIGHT = 50;
|
||||
/** 拥有装备行内图标的尺寸 */
|
||||
const OWNED_ICON_SIZE = 40;
|
||||
/** 拖拽浮层图标的尺寸 */
|
||||
const DRAG_ICON_SIZE = 44;
|
||||
/** 判定为拖拽所需的最小移动距离(像素) */
|
||||
const DRAG_THRESHOLD = 8;
|
||||
|
||||
/** 区域标题字体 */
|
||||
const titleFont = Font.defaults({ size: 15, weight: 700 });
|
||||
/** 属性名与描述文本字体 */
|
||||
const labelFont = Font.defaults({ size: 13 });
|
||||
|
||||
/** 装备界面组件 */
|
||||
export const EquipBox = defineComponent<EquipBoxProps>(props => {
|
||||
/** 勇士状态对象 */
|
||||
const hero = props.heroState;
|
||||
|
||||
/** 当前选中的装备实例 uid,-1 表示未选中 */
|
||||
const selected = ref(-1);
|
||||
/** 被拖拽的装备实例 uid,-1 表示当前没有拖拽 */
|
||||
const dragUid = ref(-1);
|
||||
/** 拖拽来源的装备槽索引,-1 表示来自拥有装备列表 */
|
||||
const dragFromSlot = ref(-1);
|
||||
/** 拖拽起点的绝对横坐标 */
|
||||
const dragStartX = ref(0);
|
||||
/** 拖拽起点的绝对纵坐标 */
|
||||
const dragStartY = ref(0);
|
||||
/** 指针当前的绝对横坐标 */
|
||||
const dragX = ref(0);
|
||||
/** 指针当前的绝对纵坐标 */
|
||||
const dragY = ref(0);
|
||||
/** 本次按下是否已越过阈值进入拖拽 */
|
||||
const dragging = ref(false);
|
||||
/** 指针当前悬停的装备槽索引,-1 表示未悬停在任何槽 */
|
||||
const hoverSlot = ref(-1);
|
||||
|
||||
/**
|
||||
* 获取装备可装备至的同类型第一个槽位索引,找不到时返回 -1
|
||||
* @param uid 装备实例 uid
|
||||
*/
|
||||
const getTargetSlot = (uid: number) => {
|
||||
for (let i = 0; i < hero.equip.slots.length; i++) {
|
||||
if (hero.equip.canEquipTo(uid, i) !== EquipStatus.CannotEquip) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
/** 拥有装备列表,按装备道具图块数字升序排列 */
|
||||
const owned = computed(() => {
|
||||
const list = hero.items.equipment.instances();
|
||||
return list.slice().sort((a, b) => a.item.num - b.item.num);
|
||||
});
|
||||
|
||||
/** 各装备槽当前所装备的实例,未装备的槽为 null */
|
||||
const equippedList = computed(() => {
|
||||
return hero.equip.slots.map((_, i) => {
|
||||
const uid = hero.equip.getEquipped(i);
|
||||
if (isNil(uid)) return null;
|
||||
return hero.items.equipment.get(uid);
|
||||
});
|
||||
});
|
||||
|
||||
/** 当前选中的装备实例,未选中时为 null */
|
||||
const selectedEquip = computed(() => {
|
||||
if (selected.value < 0) return null;
|
||||
return hero.items.equipment.get(selected.value);
|
||||
});
|
||||
|
||||
/** 选中装备所处的装备槽索引,未装备时为 -1 */
|
||||
const selectedSlot = computed(() => {
|
||||
if (selected.value < 0) return -1;
|
||||
for (let i = 0; i < hero.equip.slots.length; i++) {
|
||||
if (hero.equip.getEquipped(i) === selected.value) return i;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
|
||||
// 未装备的装备与同类型第一个槽位对比;已装备的装备仅展示卸下带来的增减
|
||||
const diff = computed<Readonly<Partial<IHeroAttr>> | null>(() => {
|
||||
if (selected.value < 0) return null;
|
||||
const equippedSlot = selectedSlot.value;
|
||||
if (equippedSlot >= 0) {
|
||||
return hero.equip.compareEquip(-1, selected.value, equippedSlot);
|
||||
}
|
||||
const target = getTargetSlot(selected.value);
|
||||
if (target < 0) return null;
|
||||
const existing = hero.equip.getEquipped(target);
|
||||
const another = isNil(existing) ? -1 : existing;
|
||||
return hero.equip.compareEquip(selected.value, another, target);
|
||||
});
|
||||
|
||||
/** 被拖拽装备的图块数字,未拖拽时为 -1 */
|
||||
const dragNum = computed(() => {
|
||||
if (dragUid.value < 0) return -1;
|
||||
const equip = hero.items.equipment.get(dragUid.value);
|
||||
return equip ? equip.item.num : -1;
|
||||
});
|
||||
|
||||
/**
|
||||
* 开始跟踪一次装备按下,记录起点并按来源区分装备槽与拥有装备
|
||||
* @param uid 装备实例 uid
|
||||
* @param fromSlot 装备所处的装备槽索引,来自拥有装备列表时为 -1
|
||||
* @param x 按下点的绝对横坐标
|
||||
* @param y 按下点的绝对纵坐标
|
||||
*/
|
||||
const startPress = (
|
||||
uid: number,
|
||||
fromSlot: number,
|
||||
x: number,
|
||||
y: number
|
||||
) => {
|
||||
dragUid.value = uid;
|
||||
dragFromSlot.value = fromSlot;
|
||||
dragStartX.value = x;
|
||||
dragStartY.value = y;
|
||||
dragX.value = x;
|
||||
dragY.value = y;
|
||||
dragging.value = false;
|
||||
hoverSlot.value = -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* 跟踪指针移动,越过阈值后进入拖拽态并更新浮层位置
|
||||
* @param x 指针当前的绝对横坐标
|
||||
* @param y 指针当前的绝对纵坐标
|
||||
*/
|
||||
const movePress = (x: number, y: number) => {
|
||||
if (dragUid.value < 0) return;
|
||||
dragX.value = x;
|
||||
dragY.value = y;
|
||||
const dx = x - dragStartX.value;
|
||||
const dy = y - dragStartY.value;
|
||||
if (dx * dx + dy * dy > DRAG_THRESHOLD * DRAG_THRESHOLD) {
|
||||
dragging.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 释放指针:未越过阈值视为点击选中;越过阈值则按来源执行穿脱
|
||||
const endPress = () => {
|
||||
if (dragUid.value < 0) return;
|
||||
const uid = dragUid.value;
|
||||
const fromSlot = dragFromSlot.value;
|
||||
const target = hoverSlot.value;
|
||||
const wasDragging = dragging.value;
|
||||
dragUid.value = -1;
|
||||
dragFromSlot.value = -1;
|
||||
dragging.value = false;
|
||||
hoverSlot.value = -1;
|
||||
if (!wasDragging) {
|
||||
// 再次点击同一件未装备的装备,将它穿到同类型第一个槽位
|
||||
if (fromSlot < 0 && selected.value === uid) {
|
||||
const slot = getTargetSlot(uid);
|
||||
if (slot >= 0) hero.equip.equip(uid, slot, true);
|
||||
}
|
||||
selected.value = uid;
|
||||
return;
|
||||
}
|
||||
if (fromSlot >= 0) {
|
||||
// 拖拽已装备的装备,释放时卸下
|
||||
hero.equip.unequip(fromSlot);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
target >= 0 &&
|
||||
hero.equip.canEquipTo(uid, target) !== EquipStatus.CannotEquip
|
||||
) {
|
||||
hero.equip.equip(uid, target, true);
|
||||
}
|
||||
};
|
||||
|
||||
/** 关闭当前装备界面 */
|
||||
const exit = () => props.controller.close(props.instance);
|
||||
|
||||
return () => {
|
||||
const equip = selectedEquip.value;
|
||||
const d = diff.value;
|
||||
return (
|
||||
<container
|
||||
loc={FULL_LOC}
|
||||
onMove={ev => movePress(ev.absoluteX, ev.absoluteY)}
|
||||
onUp={endPress}
|
||||
>
|
||||
{/* 状态区 */}
|
||||
<text
|
||||
text="状态"
|
||||
loc={[8, 8]}
|
||||
anc={[0, 0]}
|
||||
font={titleFont}
|
||||
fillStyle="gold"
|
||||
/>
|
||||
<Scroll
|
||||
noscroll
|
||||
loc={[
|
||||
0,
|
||||
STATUS_HEADER,
|
||||
STATUS_WIDTH,
|
||||
MAIN_HEIGHT - STATUS_HEADER
|
||||
]}
|
||||
>
|
||||
{props.attributes.map((attr, i) => {
|
||||
const value = hero.attribute.getFinalAttribute(attr.id);
|
||||
let delta = 0;
|
||||
if (d) {
|
||||
const raw = d[attr.id];
|
||||
if (typeof raw === 'number') delta = raw;
|
||||
}
|
||||
const valueX =
|
||||
delta === 0 ? STATUS_WIDTH - 8 : STATUS_WIDTH - 60;
|
||||
return (
|
||||
<container
|
||||
key={attr.id}
|
||||
loc={[
|
||||
0,
|
||||
i * STATUS_ROW_HEIGHT,
|
||||
STATUS_WIDTH,
|
||||
STATUS_ROW_HEIGHT
|
||||
]}
|
||||
>
|
||||
<text
|
||||
text={attr.name}
|
||||
loc={[8, STATUS_ROW_HEIGHT / 2]}
|
||||
anc={[0, 0.5]}
|
||||
font={labelFont}
|
||||
fillStyle="#dddddd"
|
||||
/>
|
||||
<text
|
||||
text={String(value)}
|
||||
loc={[valueX, STATUS_ROW_HEIGHT / 2]}
|
||||
anc={[1, 0.5]}
|
||||
font={labelFont}
|
||||
fillStyle="#ffffff"
|
||||
/>
|
||||
{delta !== 0 && (
|
||||
<text
|
||||
text={`(${delta > 0 ? '+' : ''}${delta})`}
|
||||
loc={[
|
||||
STATUS_WIDTH - 8,
|
||||
STATUS_ROW_HEIGHT / 2
|
||||
]}
|
||||
anc={[1, 0.5]}
|
||||
font={labelFont}
|
||||
fillStyle={
|
||||
delta > 0 ? '#57ff78' : '#ff5a5a'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</container>
|
||||
);
|
||||
})}
|
||||
</Scroll>
|
||||
|
||||
{/* 描述区 */}
|
||||
<container
|
||||
loc={[RIGHT_LEFT, DESC_TOP, RIGHT_WIDTH, DESC_HEIGHT]}
|
||||
>
|
||||
{!equip && (
|
||||
<text
|
||||
text="未选择装备"
|
||||
loc={[8, 12]}
|
||||
anc={[0, 0]}
|
||||
font={labelFont}
|
||||
fillStyle="#888888"
|
||||
/>
|
||||
)}
|
||||
{equip && (
|
||||
<text
|
||||
text={equip.item.name}
|
||||
loc={[8, 12]}
|
||||
anc={[0, 0]}
|
||||
font={titleFont}
|
||||
fillStyle="gold"
|
||||
/>
|
||||
)}
|
||||
{equip && (
|
||||
<text
|
||||
text={`装备孔:${equip.item.equip.slots.join(' / ')}`}
|
||||
loc={[8, 36]}
|
||||
anc={[0, 0]}
|
||||
font={labelFont}
|
||||
fillStyle="#bbbbbb"
|
||||
/>
|
||||
)}
|
||||
{equip && (
|
||||
<Scroll
|
||||
noscroll
|
||||
loc={[0, 56, RIGHT_WIDTH, DESC_HEIGHT - 56]}
|
||||
>
|
||||
<TextContent
|
||||
text={equip.item.text}
|
||||
width={RIGHT_WIDTH - 16}
|
||||
loc={[8, 4, RIGHT_WIDTH - 16, DESC_HEIGHT - 60]}
|
||||
font={labelFont}
|
||||
fillStyle="#dddddd"
|
||||
/>
|
||||
</Scroll>
|
||||
)}
|
||||
</container>
|
||||
|
||||
{/* 装备槽区 */}
|
||||
<container
|
||||
loc={[RIGHT_LEFT, SLOT_TOP, RIGHT_WIDTH, SLOT_HEIGHT]}
|
||||
>
|
||||
<Scroll hor noscroll loc={[0, 0, RIGHT_WIDTH, SLOT_HEIGHT]}>
|
||||
{hero.equip.slots.map((name, i) => {
|
||||
const slotEquip = equippedList.value[i];
|
||||
const isSelected =
|
||||
!!slotEquip && slotEquip.uid === selected.value;
|
||||
return (
|
||||
<container
|
||||
key={i}
|
||||
loc={[
|
||||
i * SLOT_CELL_WIDTH,
|
||||
0,
|
||||
SLOT_CELL_WIDTH,
|
||||
SLOT_HEIGHT
|
||||
]}
|
||||
cursor="pointer"
|
||||
onEnter={() => {
|
||||
hoverSlot.value = i;
|
||||
}}
|
||||
onLeave={() => {
|
||||
if (hoverSlot.value === i) {
|
||||
hoverSlot.value = -1;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<g-rectr
|
||||
loc={[
|
||||
4,
|
||||
4,
|
||||
SLOT_CELL_WIDTH - 8,
|
||||
SLOT_HEIGHT - 8
|
||||
]}
|
||||
circle={[6]}
|
||||
fill
|
||||
stroke
|
||||
fillStyle="rgba(255, 255, 255, 0.08)"
|
||||
strokeStyle={
|
||||
isSelected ? 'gold' : '#666666'
|
||||
}
|
||||
lineWidth={isSelected ? 2 : 1}
|
||||
/>
|
||||
{slotEquip && (
|
||||
<icon
|
||||
icon={slotEquip.item.num}
|
||||
loc={[
|
||||
(SLOT_CELL_WIDTH -
|
||||
SLOT_ICON_SIZE) /
|
||||
2,
|
||||
14,
|
||||
SLOT_ICON_SIZE,
|
||||
SLOT_ICON_SIZE
|
||||
]}
|
||||
cursor="pointer"
|
||||
onDown={ev =>
|
||||
startPress(
|
||||
slotEquip.uid,
|
||||
i,
|
||||
ev.absoluteX,
|
||||
ev.absoluteY
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<text
|
||||
text={name}
|
||||
loc={[
|
||||
SLOT_CELL_WIDTH / 2,
|
||||
SLOT_HEIGHT - 12
|
||||
]}
|
||||
anc={[0.5, 0.5]}
|
||||
font={labelFont}
|
||||
fillStyle="#cccccc"
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
})}
|
||||
</Scroll>
|
||||
</container>
|
||||
|
||||
{/* 拥有装备区 */}
|
||||
<container
|
||||
loc={[RIGHT_LEFT, OWNED_TOP, RIGHT_WIDTH, OWNED_HEIGHT]}
|
||||
>
|
||||
<text
|
||||
text="拥有装备"
|
||||
loc={[8, 2]}
|
||||
anc={[0, 0]}
|
||||
font={titleFont}
|
||||
fillStyle="gold"
|
||||
/>
|
||||
<Scroll
|
||||
noscroll
|
||||
loc={[
|
||||
0,
|
||||
OWNED_HEADER,
|
||||
RIGHT_WIDTH,
|
||||
OWNED_HEIGHT - OWNED_HEADER
|
||||
]}
|
||||
>
|
||||
{owned.value.map((item, i) => {
|
||||
const isSelected = item.uid === selected.value;
|
||||
return (
|
||||
<container
|
||||
key={item.uid}
|
||||
loc={[
|
||||
0,
|
||||
i * OWNED_ROW_HEIGHT,
|
||||
RIGHT_WIDTH,
|
||||
OWNED_ROW_HEIGHT
|
||||
]}
|
||||
cursor="pointer"
|
||||
onDown={ev =>
|
||||
startPress(
|
||||
item.uid,
|
||||
-1,
|
||||
ev.absoluteX,
|
||||
ev.absoluteY
|
||||
)
|
||||
}
|
||||
>
|
||||
<g-rectr
|
||||
loc={[
|
||||
4,
|
||||
2,
|
||||
RIGHT_WIDTH - 8,
|
||||
OWNED_ROW_HEIGHT - 4
|
||||
]}
|
||||
circle={[4]}
|
||||
fill
|
||||
fillStyle={
|
||||
isSelected
|
||||
? 'rgba(255, 204, 0, 0.28)'
|
||||
: 'rgba(255, 255, 255, 0.05)'
|
||||
}
|
||||
/>
|
||||
<icon
|
||||
icon={item.item.num}
|
||||
loc={[
|
||||
10,
|
||||
6,
|
||||
OWNED_ICON_SIZE,
|
||||
OWNED_ICON_SIZE
|
||||
]}
|
||||
/>
|
||||
<text
|
||||
text={item.item.name}
|
||||
loc={[58, OWNED_ROW_HEIGHT / 2]}
|
||||
anc={[0, 0.5]}
|
||||
font={labelFont}
|
||||
fillStyle="#eeeeee"
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
})}
|
||||
</Scroll>
|
||||
</container>
|
||||
|
||||
{/* 右下角退出按钮 */}
|
||||
<text
|
||||
text="退出"
|
||||
loc={[MAIN_WIDTH - 12, MAIN_HEIGHT - 10]}
|
||||
anc={[1, 1]}
|
||||
font={titleFont}
|
||||
cursor="pointer"
|
||||
fillStyle="#ffffff"
|
||||
onClick={exit}
|
||||
/>
|
||||
|
||||
{/* 拖拽浮层 */}
|
||||
{dragging.value && (
|
||||
<icon
|
||||
icon={dragNum.value}
|
||||
loc={[
|
||||
dragX.value - DRAG_ICON_SIZE / 2,
|
||||
dragY.value - DRAG_ICON_SIZE / 2,
|
||||
DRAG_ICON_SIZE,
|
||||
DRAG_ICON_SIZE
|
||||
]}
|
||||
noevent
|
||||
zIndex={100}
|
||||
/>
|
||||
)}
|
||||
</container>
|
||||
);
|
||||
};
|
||||
}, equipBoxProps);
|
||||
|
||||
/** 装备界面的 UI 控制器 */
|
||||
export const EquipBoxUI = new GameUI('equipbox', EquipBox);
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
UIPropsBaseCreator
|
||||
} from '@motajs/system';
|
||||
import { IClientBase, IGameUIPropsBase } from '@user/client-base';
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
/**
|
||||
* 创建游戏 UI 基础参数的构造函数
|
||||
@@ -20,7 +21,7 @@ export function createUIPropsBase(
|
||||
return {
|
||||
instance,
|
||||
controller,
|
||||
state
|
||||
state: markRaw(state)
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,17 +1,2 @@
|
||||
import { createMainController } from './controller';
|
||||
|
||||
export function createUI() {
|
||||
createMainController();
|
||||
}
|
||||
|
||||
export * from './types';
|
||||
export * from './save';
|
||||
export * from './statistics';
|
||||
export * from './viewmap';
|
||||
export * from './controller';
|
||||
export * from './background';
|
||||
export * from './func';
|
||||
export * from './main';
|
||||
export * from './settings';
|
||||
export * from './statusBar';
|
||||
export * from './toolbar';
|
||||
@@ -1,157 +0,0 @@
|
||||
import { DefaultProps } from '@motajs/render-vue';
|
||||
import { GameUI, SetupComponentOptions } from '@motajs/system';
|
||||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
FULL_LOC,
|
||||
LOAD_BYTE_HEIGHT,
|
||||
LOAD_BYTE_LENGTH,
|
||||
LOAD_BYTE_LINE_WIDTH,
|
||||
LOAD_FONT_COLOR,
|
||||
LOAD_LOADED_COLOR,
|
||||
LOAD_TASK_CENTER_HEIGHT,
|
||||
LOAD_TASK_LINE_WIDTH,
|
||||
LOAD_TASK_RADIUS,
|
||||
LOAD_UNLOADED_COLOR,
|
||||
MAIN_WIDTH
|
||||
} from '../shared';
|
||||
import { ElementLocator, Font, MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { transitioned } from '../render/use';
|
||||
import { cosh, CurveMode, linear } from '@motajs/animate';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { sleep } from '@motajs/common';
|
||||
import { GameTitleUI } from './title';
|
||||
import { IUIPropsBase } from './types';
|
||||
|
||||
export interface ILoadProps extends IUIPropsBase, DefaultProps {}
|
||||
|
||||
const loadSceneProps = {
|
||||
props: ['controller', 'instance', 'state']
|
||||
} satisfies SetupComponentOptions<ILoadProps>;
|
||||
|
||||
export const LoadScene = defineComponent<ILoadProps>(props => {
|
||||
const loader = props.state.loader;
|
||||
const taskFont = new Font('Verdana', 24);
|
||||
const byteFont = new Font('Verdana', 12);
|
||||
|
||||
/** 当前加载进度 */
|
||||
const taskProgress = transitioned(0, 500, cosh(2, CurveMode.EaseOut))!;
|
||||
const byteProgress = transitioned(0, 500, cosh(2, CurveMode.EaseOut))!;
|
||||
const alpha = transitioned(1, 400, linear())!;
|
||||
|
||||
// 两个进度条的位置
|
||||
const taskLoc: ElementLocator = [
|
||||
MAIN_WIDTH / 2,
|
||||
LOAD_TASK_CENTER_HEIGHT,
|
||||
LOAD_TASK_RADIUS * 2 + LOAD_TASK_LINE_WIDTH * 2,
|
||||
LOAD_TASK_RADIUS * 2 + LOAD_TASK_LINE_WIDTH * 2,
|
||||
0.5,
|
||||
0.5
|
||||
];
|
||||
const byteLoc: ElementLocator = [
|
||||
MAIN_WIDTH / 2,
|
||||
LOAD_BYTE_HEIGHT,
|
||||
LOAD_BYTE_LENGTH + LOAD_BYTE_LINE_WIDTH,
|
||||
LOAD_BYTE_LINE_WIDTH * 2 + byteFont.size,
|
||||
0.5,
|
||||
0.5
|
||||
];
|
||||
|
||||
const loadEnd = async () => {
|
||||
alpha.set(0);
|
||||
await sleep(400);
|
||||
props.controller.closeAll();
|
||||
props.controller.open(GameTitleUI, {});
|
||||
};
|
||||
|
||||
const startLoad = async () => {
|
||||
for await (const _ of loader.start()) {
|
||||
taskProgress.set(loader.manager.getLoadedTasks());
|
||||
byteProgress.set(loader.manager.getLoadedByte());
|
||||
}
|
||||
loadEnd();
|
||||
};
|
||||
|
||||
// 开始加载
|
||||
startLoad();
|
||||
|
||||
/** 渲染加载任务进度 */
|
||||
const renderTaskList = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const ctx = canvas.ctx;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = LOAD_TASK_LINE_WIDTH;
|
||||
ctx.font = taskFont.string();
|
||||
const loaded = loader.manager.getLoadedTasks();
|
||||
const total = loader.manager.getAddedTasks();
|
||||
// 这里使用渐变参数,因为要有动画效果
|
||||
const progress = clamp(taskProgress.value / total, 0, 1);
|
||||
const cx = taskLoc[2]! / 2;
|
||||
const cy = taskLoc[3]! / 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, LOAD_TASK_RADIUS, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = LOAD_UNLOADED_COLOR;
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
const end = progress * Math.PI * 2 - Math.PI / 2;
|
||||
ctx.arc(cx, cy, LOAD_TASK_RADIUS, -Math.PI / 2, end);
|
||||
ctx.strokeStyle = LOAD_LOADED_COLOR;
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = LOAD_FONT_COLOR;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(`${loaded} / ${total}`, cx, cy + 3);
|
||||
};
|
||||
|
||||
/** 渲染加载字节进度 */
|
||||
const renderByteList = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const ctx = canvas.ctx;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = LOAD_BYTE_LINE_WIDTH;
|
||||
ctx.font = byteFont.string();
|
||||
const total = loader.manager.getTotalByte();
|
||||
const loaded = loader.manager.getLoadedByte();
|
||||
// 这里使用渐变参数,因为要有动画效果
|
||||
const progress = clamp(byteProgress.value / total, 0, 1);
|
||||
const sx = LOAD_BYTE_LINE_WIDTH;
|
||||
const sy = byteFont.size + LOAD_BYTE_LINE_WIDTH;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(sx, sy);
|
||||
ctx.lineTo(sx + LOAD_BYTE_LENGTH, sy);
|
||||
ctx.strokeStyle = LOAD_UNLOADED_COLOR;
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(sx, sy);
|
||||
ctx.lineTo(sx + progress * LOAD_BYTE_LENGTH, sy);
|
||||
ctx.strokeStyle = LOAD_LOADED_COLOR;
|
||||
ctx.stroke();
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textBaseline = 'bottom';
|
||||
ctx.fillStyle = LOAD_FONT_COLOR;
|
||||
const loadedMB = (loaded / 2 ** 20).toFixed(2);
|
||||
const totalMB = (total / 2 ** 20).toFixed(2);
|
||||
const percent = loader.manager.getByteRatio() * 100;
|
||||
ctx.fillText(
|
||||
`${loadedMB}MB / ${totalMB}MB | ${percent.toFixed(2)}%`,
|
||||
byteLoc[2]! - LOAD_BYTE_LINE_WIDTH,
|
||||
byteLoc[3]! - LOAD_BYTE_LINE_WIDTH * 2
|
||||
);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container loc={FULL_LOC} alpha={alpha.ref.value}>
|
||||
<custom
|
||||
loc={taskLoc}
|
||||
render={renderTaskList}
|
||||
bindings={[taskProgress.ref]}
|
||||
nocache
|
||||
/>
|
||||
<custom
|
||||
loc={byteLoc}
|
||||
render={renderByteList}
|
||||
bindings={[byteProgress.ref]}
|
||||
nocache
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
}, loadSceneProps);
|
||||
|
||||
export const LoadSceneUI = new GameUI('load-scene', LoadScene);
|
||||
@@ -1,222 +0,0 @@
|
||||
import {
|
||||
Font,
|
||||
IActionEvent,
|
||||
MotaOffscreenCanvas2D,
|
||||
CustomRenderItem
|
||||
} from '@motajs/render';
|
||||
// import { WeatherController } from '../weather';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { Textbox, TextboxProps, Tip } from '@user/client-base';
|
||||
import { GameUI, SetupComponentOptions } from '@motajs/system';
|
||||
import {
|
||||
ENABLE_RIGHT_STATUS_BAR,
|
||||
MAIN_HEIGHT,
|
||||
MAIN_WIDTH,
|
||||
MAP_HEIGHT,
|
||||
MAP_WIDTH,
|
||||
RIGHT_STATUS_POS,
|
||||
STATUS_BAR_HEIGHT,
|
||||
STATUS_BAR_WIDTH
|
||||
} from '../shared';
|
||||
import { LeftStatusBar, RightStatusBar } from './statusBar';
|
||||
import { state } from '@user/data-state';
|
||||
import { mainUIController } from './controller';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { using } from '../render/renderer';
|
||||
import { IUIPropsBase } from './types';
|
||||
|
||||
interface MainSceneProps extends IUIPropsBase {}
|
||||
|
||||
const mainSceneProps = {
|
||||
props: ['controller', 'instance', 'state']
|
||||
} satisfies SetupComponentOptions<MainSceneProps>;
|
||||
|
||||
const MainScene = defineComponent<MainSceneProps>(props => {
|
||||
const mainMapRenderer = props.state.mainMapRenderer;
|
||||
const mainMapExtension = props.state.mainMapExtension;
|
||||
|
||||
//#region 基本定义
|
||||
|
||||
const mainTextboxProps: TextboxProps = {
|
||||
text: '',
|
||||
hidden: true,
|
||||
loc: [0, MAP_HEIGHT - 150, MAP_WIDTH, 150],
|
||||
zIndex: 30,
|
||||
fillStyle: '#fff',
|
||||
titleFill: 'gold',
|
||||
font: new Font('normal'),
|
||||
titleFont: new Font('normal', 20, 'px', 700),
|
||||
winskin: 'winskin.png',
|
||||
interval: 30,
|
||||
lineHeight: 4,
|
||||
width: MAP_WIDTH
|
||||
};
|
||||
|
||||
const hideStatus = ref(
|
||||
props.state.flags.getFieldValueDefaults('hideStatusBar', false)
|
||||
);
|
||||
// TODO: flags 更新未接(当前无监听系统)
|
||||
// const weather = new WeatherController();
|
||||
// weather.extern('main');
|
||||
|
||||
//#region sprite 渲染
|
||||
|
||||
let lastLength = 0;
|
||||
using.onExcitedFunc(() => {
|
||||
const len = core.status.stepPostfix?.length ?? 0;
|
||||
if (len !== lastLength) {
|
||||
mapMiscSprite.value?.update();
|
||||
lastLength = len;
|
||||
}
|
||||
});
|
||||
|
||||
const mapMiscSprite = ref<CustomRenderItem>();
|
||||
|
||||
const renderMapMisc = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const step = core.status.stepPostfix;
|
||||
if (!step) return;
|
||||
const ctx = canvas.ctx;
|
||||
ctx.fillStyle = '#fff';
|
||||
step.forEach(({ x, y, direction }) => {
|
||||
ctx.fillRect(x * 32 + 12, y * 32 + 12, 8, 8);
|
||||
if (!isNil(direction)) {
|
||||
switch (direction) {
|
||||
case 'down':
|
||||
ctx.fillRect(x * 32 + 12, y * 32 + 20, 8, 12);
|
||||
break;
|
||||
case 'left':
|
||||
ctx.fillRect(x * 32, y * 32 + 12, 12, 8);
|
||||
break;
|
||||
case 'right':
|
||||
ctx.fillRect(x * 32 + 20, y * 32 + 12, 12, 8);
|
||||
break;
|
||||
case 'up':
|
||||
ctx.fillRect(x * 32 + 12, y * 32, 8, 12);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//#region 交互监听
|
||||
|
||||
/**
|
||||
* 对于 registerAction 的 fallback
|
||||
*/
|
||||
const clickMap = (ev: IActionEvent) => {
|
||||
const bx = Math.floor(ev.offsetX / 32);
|
||||
const by = Math.floor(ev.offsetY / 32);
|
||||
core.doRegisteredAction('onup', bx, by, ev.offsetX, ev.offsetY);
|
||||
};
|
||||
|
||||
/**
|
||||
* 对于 registerAction 的 fallback
|
||||
*/
|
||||
const downMap = (ev: IActionEvent) => {
|
||||
const bx = Math.floor(ev.offsetX / 32);
|
||||
const by = Math.floor(ev.offsetY / 32);
|
||||
core.doRegisteredAction('ondown', bx, by, ev.offsetX, ev.offsetY);
|
||||
};
|
||||
|
||||
/**
|
||||
* 对于 registerAction 的 fallback
|
||||
*/
|
||||
const moveMap = (ev: IActionEvent) => {
|
||||
const bx = Math.floor(ev.offsetX / 32);
|
||||
const by = Math.floor(ev.offsetY / 32);
|
||||
core.doRegisteredAction('onmove', bx, by, ev.offsetX, ev.offsetY);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container
|
||||
id="main-scene"
|
||||
width={MAIN_WIDTH}
|
||||
height={MAIN_HEIGHT}
|
||||
noanti
|
||||
nocache
|
||||
>
|
||||
<LeftStatusBar
|
||||
loc={[0, 0, STATUS_BAR_WIDTH, STATUS_BAR_HEIGHT]}
|
||||
hidden={hideStatus.value}
|
||||
state={props.state}
|
||||
></LeftStatusBar>
|
||||
<g-line
|
||||
line={[STATUS_BAR_WIDTH, 0, STATUS_BAR_WIDTH, MAIN_HEIGHT]}
|
||||
lineWidth={1}
|
||||
/>
|
||||
<container
|
||||
id="map-draw"
|
||||
loc={[STATUS_BAR_WIDTH, 0, MAP_WIDTH, MAP_HEIGHT]}
|
||||
zIndex={10}
|
||||
onClick={clickMap}
|
||||
onDown={downMap}
|
||||
onMove={moveMap}
|
||||
noanti
|
||||
>
|
||||
<map-render
|
||||
renderer={mainMapRenderer}
|
||||
// @ts-expect-error 需要重构
|
||||
layerState={state.maps}
|
||||
extension={mainMapExtension}
|
||||
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}
|
||||
/>
|
||||
<Textbox id="main-textbox" {...mainTextboxProps}></Textbox>
|
||||
{/* <FloorChange id="floor-change" zIndex={50}></FloorChange> */}
|
||||
<Tip
|
||||
id="main-tip"
|
||||
zIndex={80}
|
||||
loc={[8, 8, 200, 32]}
|
||||
pad={[12, 6]}
|
||||
corner={16}
|
||||
/>
|
||||
<custom
|
||||
noevent
|
||||
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}
|
||||
ref={mapMiscSprite}
|
||||
zIndex={170}
|
||||
render={renderMapMisc}
|
||||
/>
|
||||
</container>
|
||||
<g-line
|
||||
line={[RIGHT_STATUS_POS, 0, RIGHT_STATUS_POS, MAP_HEIGHT]}
|
||||
lineWidth={1}
|
||||
/>
|
||||
<RightStatusBar
|
||||
loc={[RIGHT_STATUS_POS, 0, STATUS_BAR_WIDTH, STATUS_BAR_HEIGHT]}
|
||||
hidden={hideStatus.value && ENABLE_RIGHT_STATUS_BAR}
|
||||
state={props.state}
|
||||
></RightStatusBar>
|
||||
<container
|
||||
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
|
||||
hidden={!mainUIController.active.value}
|
||||
zIndex={200}
|
||||
>
|
||||
{mainUIController.render()}
|
||||
</container>
|
||||
<g-rect
|
||||
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
|
||||
hidden={hideStatus.value}
|
||||
zIndex={100}
|
||||
stroke
|
||||
noevent
|
||||
></g-rect>
|
||||
<g-line
|
||||
line={[STATUS_BAR_WIDTH, 0, RIGHT_STATUS_POS, 0]}
|
||||
hidden={!hideStatus.value}
|
||||
zIndex={100}
|
||||
/>
|
||||
<g-line
|
||||
line={[
|
||||
STATUS_BAR_WIDTH,
|
||||
MAP_HEIGHT,
|
||||
RIGHT_STATUS_POS,
|
||||
MAP_HEIGHT
|
||||
]}
|
||||
hidden={!hideStatus.value}
|
||||
zIndex={100}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
}, mainSceneProps);
|
||||
|
||||
export const MainSceneUI = new GameUI('main-scene', MainScene);
|
||||
@@ -1,615 +0,0 @@
|
||||
import { ElementLocator, IWheelEvent, Font } from '@motajs/render';
|
||||
import { DefaultProps } from '@motajs/render-vue';
|
||||
import {
|
||||
GameUI,
|
||||
IUIController,
|
||||
SetupComponentOptions,
|
||||
IUIDefaultPropsBase
|
||||
} from '@motajs/system';
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
computed,
|
||||
onMounted,
|
||||
shallowReactive
|
||||
} from 'vue';
|
||||
import {
|
||||
getConfirm,
|
||||
Page,
|
||||
PageExpose,
|
||||
Thumbnail,
|
||||
TipStore,
|
||||
IGameUIPropsBase
|
||||
} from '@user/client-base';
|
||||
import { useKey } from '../render/use';
|
||||
import {
|
||||
HALF_HEIGHT,
|
||||
HALF_WIDTH,
|
||||
MAP_WIDTH,
|
||||
POP_BOX_WIDTH,
|
||||
SAVE_DOWN_PAD,
|
||||
SAVE_INTERVAL,
|
||||
SAVE_ITEM_DOWN,
|
||||
SAVE_ITEM_HEIGHT,
|
||||
SAVE_ITEM_SIZE,
|
||||
SAVE_ITEM_TOP,
|
||||
SAVE_PAGES
|
||||
} from '../shared';
|
||||
import { adjustGrid, IGridLayoutData } from '@user/client-base';
|
||||
import { getSave, SaveData } from '../render/utils';
|
||||
|
||||
export const enum SaveMode {
|
||||
Save,
|
||||
Load,
|
||||
Other
|
||||
}
|
||||
|
||||
export interface SaveProps extends IUIDefaultPropsBase, DefaultProps {
|
||||
loc: ElementLocator;
|
||||
mode: SaveMode;
|
||||
}
|
||||
|
||||
export interface SaveItemProps extends DefaultProps {
|
||||
loc: ElementLocator;
|
||||
index: number;
|
||||
selected: boolean;
|
||||
inDelete: boolean;
|
||||
data: SaveData | null;
|
||||
}
|
||||
|
||||
export type SaveEmits = {
|
||||
/** 点击存档时触发 */
|
||||
emit: (index: number, exist: boolean) => void;
|
||||
/** 删除存档时触发 */
|
||||
delete: (index: number, exist: boolean) => void;
|
||||
/** 手动点击退出时触发 */
|
||||
exit: () => void;
|
||||
};
|
||||
|
||||
const saveProps = {
|
||||
props: ['loc', 'controller', 'instance', 'mode'],
|
||||
emits: ['delete', 'emit', 'exit']
|
||||
} satisfies SetupComponentOptions<SaveProps, SaveEmits, keyof SaveEmits>;
|
||||
|
||||
const saveBtnProps = {
|
||||
props: ['loc', 'index', 'selected', 'inDelete', 'data']
|
||||
} satisfies SetupComponentOptions<SaveItemProps>;
|
||||
|
||||
export const SaveItem = defineComponent<SaveItemProps>(props => {
|
||||
const font = Font.defaults({ size: 16 });
|
||||
const statusFont = Font.defaults({ size: 12 });
|
||||
|
||||
const w = computed(() => props.loc[2] ?? 200);
|
||||
const h = computed(() => props.loc[3] ?? 200);
|
||||
const lineWidth = computed(() => (props.selected ? 4 : 2));
|
||||
const imgLoc = computed<ElementLocator>(() => {
|
||||
const size = w.value - 4;
|
||||
return [2, SAVE_ITEM_TOP, size, size];
|
||||
});
|
||||
|
||||
const name = computed(() => {
|
||||
return props.index === 0 ? '自动存档' : `存档${props.index}`;
|
||||
});
|
||||
const statusText = computed(() => {
|
||||
if (!props.data) return '';
|
||||
else {
|
||||
const hero = props.data.data.hero;
|
||||
const hp = core.formatBigNumber(hero.hp);
|
||||
const atk = core.formatBigNumber(hero.atk);
|
||||
const def = core.formatBigNumber(hero.def);
|
||||
return `${hp}/${atk}/${def}`;
|
||||
}
|
||||
});
|
||||
const timeText = computed(() => {
|
||||
if (!props.data) return '';
|
||||
const date = new Date(props.data.data.time);
|
||||
return date.toLocaleString();
|
||||
});
|
||||
|
||||
const strokeStyle = computed(() => {
|
||||
if (props.selected) return props.inDelete ? 'red' : 'gold';
|
||||
else return 'white';
|
||||
});
|
||||
|
||||
const floorId = computed(() => props.data?.data.floorId ?? 'empty');
|
||||
const mapBlocks = computed(() => {
|
||||
if (!props.data) return [];
|
||||
else {
|
||||
const currData = props.data.data;
|
||||
const map = core.maps.loadMap(currData.maps, currData.floorId);
|
||||
core.extractBlocksForUI(map, currData.hero.flags); // 这一步会向map写入blocks
|
||||
return map.blocks;
|
||||
}
|
||||
});
|
||||
|
||||
return () => (
|
||||
<container loc={props.loc}>
|
||||
<text
|
||||
text={name.value}
|
||||
font={font}
|
||||
loc={[w.value / 2, 0]}
|
||||
anc={[0.5, 0]}
|
||||
/>
|
||||
<g-rect
|
||||
loc={imgLoc.value}
|
||||
strokeAndFill
|
||||
fillStyle="gray"
|
||||
strokeStyle={strokeStyle.value}
|
||||
lineWidth={lineWidth.value}
|
||||
lineJoin="miter"
|
||||
cursor="pointer"
|
||||
/>
|
||||
<Thumbnail
|
||||
hidden={!props.data}
|
||||
loc={imgLoc.value}
|
||||
padStyle="gray"
|
||||
floorId={floorId.value}
|
||||
map={mapBlocks.value}
|
||||
hero={props.data?.data.hero}
|
||||
all
|
||||
noHD
|
||||
size={(w.value - 4) / MAP_WIDTH}
|
||||
noevent
|
||||
/>
|
||||
<text
|
||||
text={statusText.value}
|
||||
fillStyle="yellow"
|
||||
font={statusFont}
|
||||
loc={[w.value / 2, h.value - SAVE_ITEM_DOWN]}
|
||||
anc={[0.5, 0]}
|
||||
/>
|
||||
<text
|
||||
text={timeText.value}
|
||||
fillStyle="yellow"
|
||||
font={statusFont}
|
||||
loc={[w.value / 2, h.value - SAVE_ITEM_DOWN + 12]}
|
||||
anc={[0.5, 0]}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
}, saveBtnProps);
|
||||
|
||||
export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
|
||||
(props, { emit }) => {
|
||||
const font = Font.defaults({ size: 18 });
|
||||
const pageFont = Font.defaults({ size: 14 });
|
||||
|
||||
/** 当前页上被选中的存档的posIndex */
|
||||
const selected = ref(0);
|
||||
const now = ref(0);
|
||||
const inDelete = ref(false);
|
||||
const pageRef = ref<PageExpose>();
|
||||
|
||||
const saveData: Record<number, SaveData | null> = shallowReactive({});
|
||||
|
||||
const width = computed(() => props.loc[2] ?? 200);
|
||||
const height = computed(() => props.loc[3] ?? 200);
|
||||
|
||||
const grid = computed<IGridLayoutData>(() =>
|
||||
adjustGrid(
|
||||
width.value,
|
||||
height.value - SAVE_DOWN_PAD,
|
||||
SAVE_ITEM_SIZE,
|
||||
SAVE_ITEM_HEIGHT,
|
||||
SAVE_INTERVAL
|
||||
)
|
||||
);
|
||||
|
||||
const contentLoc = computed<ElementLocator>(() => {
|
||||
const cx = width.value / 2;
|
||||
const cy = (height.value - SAVE_DOWN_PAD) / 2;
|
||||
return [cx, cy, grid.value.width, grid.value.height, 0.5, 0.5];
|
||||
});
|
||||
|
||||
const deleteLoc = computed<ElementLocator>(() => {
|
||||
const pad = (width.value - grid.value.width) / 2;
|
||||
return [pad, height.value - 13, void 0, void 0, 0, 1];
|
||||
});
|
||||
|
||||
const exitLoc = computed<ElementLocator>(() => {
|
||||
const pad = (width.value - grid.value.width) / 2;
|
||||
const right = width.value - pad;
|
||||
return [right, height.value - 13, void 0, void 0, 1, 1];
|
||||
});
|
||||
|
||||
//#region 数据信息
|
||||
|
||||
/**
|
||||
* 获取存档在当前页的序号,范围为 0 到 pageCap-1。
|
||||
*/
|
||||
const getPosIndex = (index: number) => {
|
||||
if (index === -1) return 0;
|
||||
return (index % (grid.value.count - 1)) + 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取存档的总序号,从 0 开始,用于数据交互。
|
||||
*/
|
||||
const getIndex = (posIndex: number, page: number) => {
|
||||
return page * (grid.value.count - 1) + posIndex - 1;
|
||||
};
|
||||
|
||||
const updateDataList = async (page: number) => {
|
||||
const promises: Promise<SaveData | null>[] = [getSave(0)];
|
||||
for (let i = 1; i < grid.value.count; i++) {
|
||||
const index = getIndex(i, page);
|
||||
promises.push(getSave(index + 1));
|
||||
}
|
||||
const before = page;
|
||||
const data = await Promise.all(promises);
|
||||
if (now.value !== before) return;
|
||||
data.forEach((v, i) => {
|
||||
if (v) {
|
||||
saveData[i] = v;
|
||||
} else {
|
||||
saveData[i] = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const startIndex = getPosIndex(core.saves.saveIndex);
|
||||
selected.value = startIndex - 1;
|
||||
pageRef.value?.changePage(
|
||||
Math.floor(core.saves.saveIndex / (grid.value.count - 1))
|
||||
);
|
||||
updateDataList(now.value);
|
||||
});
|
||||
|
||||
//#region 逻辑操作
|
||||
|
||||
const exist = (index: number) => {
|
||||
return saveData[index] !== null;
|
||||
};
|
||||
|
||||
const deleteData = (index: number) => {
|
||||
saveData[index] = null;
|
||||
};
|
||||
|
||||
const emitSave = async (index: number) => {
|
||||
const posIndex = getPosIndex(index);
|
||||
if (inDelete.value) {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
`确认要删除存档 ${index + 1}?`,
|
||||
[HALF_WIDTH, HALF_HEIGHT, void 0, void 0, 0.5, 0.5],
|
||||
POP_BOX_WIDTH,
|
||||
{ winskin: 'winskin.png' }
|
||||
);
|
||||
if (confirm) {
|
||||
emit('delete', index, exist(posIndex));
|
||||
deleteData(posIndex);
|
||||
}
|
||||
} else {
|
||||
emit('emit', index, exist(posIndex));
|
||||
}
|
||||
if (index === -1) {
|
||||
selected.value = 0;
|
||||
} else {
|
||||
selected.value = (index % (grid.value.count - 1)) + 1;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDelete = () => {
|
||||
inDelete.value = !inDelete.value;
|
||||
};
|
||||
|
||||
const exit = () => {
|
||||
emit('exit');
|
||||
props.controller.close(props.instance);
|
||||
};
|
||||
|
||||
// #region 按键实现
|
||||
|
||||
const [key] = useKey();
|
||||
key.realize('confirm', () => {
|
||||
if (selected.value === 0) {
|
||||
emitSave(-1);
|
||||
} else {
|
||||
emitSave(
|
||||
(grid.value.count - 1) * now.value + selected.value - 1
|
||||
);
|
||||
}
|
||||
})
|
||||
.realize('exit', exit)
|
||||
.realize('save', () => {
|
||||
if (props.mode === SaveMode.Save) exit();
|
||||
})
|
||||
.realize('load', () => {
|
||||
if (props.mode === SaveMode.Load) exit();
|
||||
})
|
||||
.realize(
|
||||
'@save_pageUp',
|
||||
() => {
|
||||
pageRef.value?.movePage(1);
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@save_pageDown',
|
||||
() => {
|
||||
pageRef.value?.movePage(-1);
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@save_up',
|
||||
() => {
|
||||
if (!pageRef.value) return;
|
||||
const cols = grid.value.cols;
|
||||
const count = grid.value.count;
|
||||
if (selected.value >= cols) {
|
||||
selected.value -= cols;
|
||||
} else {
|
||||
if (now.value === 0) {
|
||||
selected.value = 0;
|
||||
} else {
|
||||
const selectedCol = selected.value % cols;
|
||||
selected.value = count - (cols - selectedCol);
|
||||
pageRef.value?.movePage(-1);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@save_down',
|
||||
() => {
|
||||
const cols = grid.value.cols;
|
||||
const count = grid.value.count;
|
||||
if (selected.value < count - cols) {
|
||||
selected.value += cols;
|
||||
} else {
|
||||
const selectedCol = selected.value % cols;
|
||||
selected.value = selectedCol;
|
||||
pageRef.value?.movePage(1);
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@save_left',
|
||||
() => {
|
||||
if (!pageRef.value) return;
|
||||
const count = grid.value.count;
|
||||
if (selected.value > 0) {
|
||||
selected.value--;
|
||||
} else {
|
||||
if (now.value > 0) {
|
||||
selected.value = count;
|
||||
pageRef.value?.movePage(-1);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@save_right',
|
||||
() => {
|
||||
const count = grid.value.count;
|
||||
if (selected.value < count) {
|
||||
selected.value++;
|
||||
} else {
|
||||
selected.value = 0;
|
||||
pageRef.value?.movePage(1);
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
);
|
||||
|
||||
//#region 事件监听
|
||||
|
||||
// todo: 按住快速切换页码
|
||||
|
||||
const wheel = (ev: IWheelEvent) => {
|
||||
const delta = Math.sign(ev.wheelY);
|
||||
if (ev.ctrlKey) {
|
||||
pageRef.value?.movePage(delta * 10);
|
||||
} else {
|
||||
pageRef.value?.movePage(delta);
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container loc={props.loc}>
|
||||
<Page
|
||||
ref={pageRef}
|
||||
loc={[0, 0, width.value, height.value - 10]}
|
||||
pages={SAVE_PAGES}
|
||||
font={pageFont}
|
||||
v-model:page={now.value}
|
||||
onWheel={wheel}
|
||||
onPageChange={updateDataList}
|
||||
>
|
||||
{(page: number) => (
|
||||
<container loc={contentLoc.value}>
|
||||
{grid.value.locs.map((v, i) => {
|
||||
const count = grid.value.count;
|
||||
const rawIndex = (count - 1) * page + i;
|
||||
const index = i === 0 ? 0 : rawIndex;
|
||||
return (
|
||||
<SaveItem
|
||||
key={index}
|
||||
loc={v}
|
||||
index={index}
|
||||
selected={selected.value === i}
|
||||
inDelete={inDelete.value}
|
||||
data={saveData[i]}
|
||||
onClick={() => emitSave(index - 1)}
|
||||
onEnter={() => (selected.value = i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</container>
|
||||
)}
|
||||
</Page>
|
||||
<text
|
||||
text="删除模式"
|
||||
loc={deleteLoc.value}
|
||||
font={font}
|
||||
zIndex={10}
|
||||
fillStyle={inDelete.value ? 'red' : 'white'}
|
||||
onClick={toggleDelete}
|
||||
cursor="pointer"
|
||||
/>
|
||||
<text
|
||||
text="返回游戏"
|
||||
loc={exitLoc.value}
|
||||
font={font}
|
||||
zIndex={10}
|
||||
onClick={exit}
|
||||
cursor="pointer"
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
},
|
||||
saveProps
|
||||
);
|
||||
|
||||
export const SaveUI = new GameUI('save', Save);
|
||||
|
||||
export interface SaveValidation {
|
||||
readonly valid: boolean;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
export type SaveValidationFunction = (
|
||||
index: number,
|
||||
exist: boolean
|
||||
) => SaveValidation;
|
||||
|
||||
/**
|
||||
* 打开存读档界面并让用户选择一个存档。如果用户手动关闭了存档界面,返回 -2,否则返回用户选择的存档索引。
|
||||
* 参数参考 {@link SaveProps},事件不可自定义。
|
||||
*
|
||||
* 使用示例:
|
||||
* ```ts
|
||||
* const index = await selectSave(props.controller, [0, 0, 416, 416]);
|
||||
* if (index === -1) {
|
||||
* // 如果用户未选择存档,而是关闭了存档。
|
||||
* } else if (index === 0) {
|
||||
* // 用户选择了自动存档。
|
||||
* } else {
|
||||
* // 用户选择了一个存档。
|
||||
* }
|
||||
* ```
|
||||
* @param controller 在哪个控制器上打开
|
||||
* @param loc 存读档界面的坐标
|
||||
* @param props 传递给存读档界面的参数
|
||||
* @returns 选择的存档索引
|
||||
*/
|
||||
export function selectSave(
|
||||
controller: IUIController<IGameUIPropsBase>,
|
||||
loc: ElementLocator,
|
||||
mode: SaveMode,
|
||||
validate?: SaveValidationFunction,
|
||||
props?: SaveProps
|
||||
) {
|
||||
const validateDelete = (index: number, exist: boolean): SaveValidation => {
|
||||
if (index === -1) {
|
||||
return { message: '不能删除自动存档!', valid: false };
|
||||
} else {
|
||||
return { message: '无法删除该存档!', valid: exist };
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise<number>(res => {
|
||||
const instance = controller.open(SaveUI, {
|
||||
loc,
|
||||
...props,
|
||||
mode,
|
||||
onEmit: (index: number, exist: boolean) => {
|
||||
if (!validate) {
|
||||
controller.close(instance);
|
||||
res(index);
|
||||
return;
|
||||
}
|
||||
const validation = validate(index, exist);
|
||||
if (validation.valid) {
|
||||
controller.close(instance);
|
||||
res(index);
|
||||
} else {
|
||||
TipStore.get('main-tip')?.drawTip(validation.message);
|
||||
}
|
||||
},
|
||||
onDelete: (index: number, exist: boolean) => {
|
||||
if (!validate) return;
|
||||
const validation = validateDelete(index, exist);
|
||||
if (validation.valid) {
|
||||
core.removeSave(index);
|
||||
} else {
|
||||
TipStore.get('main-tip')?.drawTip(validation.message);
|
||||
}
|
||||
},
|
||||
onExit: () => {
|
||||
res(-2);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveSave(
|
||||
controller: IUIController<IGameUIPropsBase>,
|
||||
loc: ElementLocator,
|
||||
props?: SaveProps
|
||||
) {
|
||||
const validate = (index: number): SaveValidation => {
|
||||
if (index === -1) {
|
||||
return { message: '不能存档至自动存档!', valid: false };
|
||||
} else {
|
||||
return { message: '', valid: true };
|
||||
}
|
||||
};
|
||||
const index = await selectSave(
|
||||
controller,
|
||||
loc,
|
||||
SaveMode.Save,
|
||||
validate,
|
||||
props
|
||||
);
|
||||
if (index === -2) return false;
|
||||
core.doSL(index + 1, 'save');
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function saveLoad(
|
||||
controller: IUIController<IGameUIPropsBase>,
|
||||
loc: ElementLocator,
|
||||
props?: SaveProps
|
||||
) {
|
||||
const validate = (_: number, exist: boolean): SaveValidation => {
|
||||
return { message: '无效的存档!', valid: exist };
|
||||
};
|
||||
const index = await selectSave(
|
||||
controller,
|
||||
loc,
|
||||
SaveMode.Load,
|
||||
validate,
|
||||
props
|
||||
);
|
||||
if (index === -2) return false;
|
||||
if (index === -1) {
|
||||
core.doSL('autoSave', 'load');
|
||||
} else {
|
||||
core.doSL(index + 1, 'load');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function saveWithExist(
|
||||
controller: IUIController<IGameUIPropsBase>,
|
||||
loc: ElementLocator,
|
||||
props?: SaveProps
|
||||
) {
|
||||
const validate = (_: number, exist: boolean): SaveValidation => {
|
||||
return { message: '无效的存档!', valid: exist };
|
||||
};
|
||||
const index = await selectSave(
|
||||
controller,
|
||||
loc,
|
||||
SaveMode.Load,
|
||||
validate,
|
||||
props
|
||||
);
|
||||
return index;
|
||||
}
|
||||
@@ -1,702 +0,0 @@
|
||||
import { ElementLocator } from '@motajs/render';
|
||||
import { GameUI, IUIController, SetupComponentOptions } from '@motajs/system';
|
||||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
ChoiceItem,
|
||||
ChoiceKey,
|
||||
Choices,
|
||||
ChoicesProps,
|
||||
getConfirm,
|
||||
waitbox,
|
||||
IGameUIPropsBase
|
||||
} from '@user/client-base';
|
||||
import { mainUi } from '@motajs/legacy-ui';
|
||||
import { gameKey, generateKeyboardEvent } from '@motajs/system';
|
||||
import { getVitualKeyOnce } from '@motajs/legacy-ui';
|
||||
import { getAllSavesData, getSaveData, syncFromServer } from '../render/utils';
|
||||
import { getInput } from '@user/client-base';
|
||||
import { openStatistics } from './statistics';
|
||||
import { saveWithExist } from './save';
|
||||
import { compressToBase64 } from 'lz-string';
|
||||
import { ViewMapUI } from './viewmap';
|
||||
import { CENTER_LOC, FULL_LOC, MAIN_HEIGHT, POP_BOX_WIDTH } from '../shared';
|
||||
import { useKey } from '../render/use';
|
||||
import { IUIPropsBase } from './types';
|
||||
|
||||
export interface MainSettingsProps extends Partial<ChoicesProps>, IUIPropsBase {
|
||||
loc: ElementLocator;
|
||||
}
|
||||
|
||||
const mainSettingsProps = {
|
||||
props: ['loc', 'controller', 'instance', 'state']
|
||||
} satisfies SetupComponentOptions<MainSettingsProps>;
|
||||
|
||||
const enum MainChoice {
|
||||
SystemSetting,
|
||||
VirtualKey,
|
||||
ViewMap,
|
||||
/** @see {@link ReplaySettings} */
|
||||
Replay,
|
||||
/** @see {@link SyncSave} */
|
||||
SyncSave,
|
||||
/** @see {@link GameInfo} */
|
||||
GameInfo,
|
||||
Restart,
|
||||
Back
|
||||
}
|
||||
|
||||
export const MainSettings = defineComponent<MainSettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[MainChoice.SystemSetting, '系统设置'],
|
||||
[MainChoice.VirtualKey, '虚拟键盘'],
|
||||
[MainChoice.ViewMap, '浏览地图'],
|
||||
[MainChoice.Replay, '录像回放'],
|
||||
[MainChoice.SyncSave, '同步存档'],
|
||||
[MainChoice.GameInfo, '游戏信息'],
|
||||
[MainChoice.Restart, '返回标题'],
|
||||
[MainChoice.Back, '返回游戏']
|
||||
];
|
||||
|
||||
const controller: IUIController<IGameUIPropsBase> = props.controller;
|
||||
const [key, scope] = useKey();
|
||||
key.realize('exit', () => props.controller.close(props.instance));
|
||||
|
||||
const choose = async (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case MainChoice.SystemSetting: {
|
||||
mainUi.open('settings');
|
||||
break;
|
||||
}
|
||||
case MainChoice.VirtualKey: {
|
||||
getVitualKeyOnce().then(value => {
|
||||
gameKey.emitKey(
|
||||
value.key,
|
||||
value.assist,
|
||||
'up',
|
||||
generateKeyboardEvent(value.key, value.assist)
|
||||
);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case MainChoice.ViewMap: {
|
||||
controller.open(ViewMapUI, {
|
||||
loc: FULL_LOC
|
||||
});
|
||||
break;
|
||||
}
|
||||
case MainChoice.Replay: {
|
||||
controller.open(ReplaySettingsUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case MainChoice.SyncSave: {
|
||||
controller.open(SyncSaveUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case MainChoice.GameInfo: {
|
||||
controller.open(GameInfoUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case MainChoice.Restart: {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'确认要返回标题吗?',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
if (confirm) {
|
||||
props.controller.closeAll();
|
||||
core.restart();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MainChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
choices={choices}
|
||||
width={POP_BOX_WIDTH}
|
||||
onChoose={choose}
|
||||
maxHeight={MAIN_HEIGHT - 64}
|
||||
interval={8}
|
||||
scope={scope}
|
||||
/>
|
||||
);
|
||||
}, mainSettingsProps);
|
||||
|
||||
const enum ReplayChoice {
|
||||
Start,
|
||||
StartFromSave,
|
||||
ResumeReplay,
|
||||
ReplayRest,
|
||||
ChooseReplay,
|
||||
Download,
|
||||
Back
|
||||
}
|
||||
|
||||
export const ReplaySettings = defineComponent<MainSettingsProps>(props => {
|
||||
const choice: ChoiceItem[] = [
|
||||
[ReplayChoice.Start, '从头回放录像'],
|
||||
[ReplayChoice.StartFromSave, '从存档开始回放'],
|
||||
[ReplayChoice.ResumeReplay, '接续播放剩余录像'],
|
||||
[ReplayChoice.ReplayRest, '播放存档剩余录像'],
|
||||
[ReplayChoice.ChooseReplay, '选择录像文件'],
|
||||
[ReplayChoice.Download, '下载当前录像'],
|
||||
[ReplayChoice.Back, '返回游戏']
|
||||
];
|
||||
|
||||
const [key, scope] = useKey();
|
||||
key.realize('exit', () => props.controller.close(props.instance));
|
||||
|
||||
const choose = async (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case ReplayChoice.Start: {
|
||||
props.controller.closeAll();
|
||||
core.ui.closePanel();
|
||||
const route = core.status.route.slice();
|
||||
// TODO: flags 更新未接(当前无监听系统)
|
||||
const flags = props.state.flags;
|
||||
const seed = flags.getFieldValue<number>('__seed__');
|
||||
core.startGame(core.status.hard, seed, route);
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.StartFromSave: {
|
||||
const index = await saveWithExist(props.controller, FULL_LOC);
|
||||
if (index === -2) break;
|
||||
if (index === -1) {
|
||||
core.doSL('autoSave', 'replayLoad');
|
||||
} else {
|
||||
core.doSL(index + 1, 'replayLoad');
|
||||
}
|
||||
props.controller.closeAll();
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.ResumeReplay: {
|
||||
const index = await saveWithExist(props.controller, FULL_LOC);
|
||||
if (index === -2) break;
|
||||
const name = index === -1 ? 'autoSave' : index + 1;
|
||||
const success = core.doSL(name, 'replayRemain');
|
||||
if (!success) {
|
||||
props.controller.closeAll();
|
||||
break;
|
||||
}
|
||||
await getConfirm(
|
||||
props.controller,
|
||||
'[步骤2]请选择第二个存档。\n\\r[yellow]该存档必须是前一个存档的后续。\\r\n将尝试播放到此存档。',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
const index2 = await saveWithExist(props.controller, FULL_LOC);
|
||||
if (index2 === -2) break;
|
||||
const name2 = index2 === -1 ? 'autoSave' : index2 + 1;
|
||||
core.doSL(name2, 'replayRemain');
|
||||
props.controller.closeAll();
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.ReplayRest: {
|
||||
const index = await saveWithExist(props.controller, FULL_LOC);
|
||||
if (index === -2) break;
|
||||
if (index === -1) {
|
||||
core.doSL('autoSave', 'replaySince');
|
||||
} else {
|
||||
core.doSL(index + 1, 'replaySince');
|
||||
}
|
||||
props.controller.closeAll();
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.ChooseReplay: {
|
||||
props.controller.closeAll();
|
||||
core.chooseReplayFile();
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.Download: {
|
||||
// TODO: flags 更新未接(当前无监听系统)
|
||||
const flags = props.state.flags;
|
||||
const seed = flags.getFieldValue<number>('__seed__');
|
||||
core.download(
|
||||
core.firstData.name + '_' + core.formatDate2() + '.h5route',
|
||||
compressToBase64(
|
||||
JSON.stringify({
|
||||
name: core.firstData.name,
|
||||
hard: core.status.hard,
|
||||
seed,
|
||||
route: core.encodeRoute(core.status.route)
|
||||
})
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
choices={choice}
|
||||
width={POP_BOX_WIDTH}
|
||||
onChoose={choose}
|
||||
interval={8}
|
||||
scope={scope}
|
||||
/>
|
||||
);
|
||||
}, mainSettingsProps);
|
||||
|
||||
const enum GameInfoChoice {
|
||||
Statistics,
|
||||
Project,
|
||||
Tower,
|
||||
Help,
|
||||
Download,
|
||||
Back
|
||||
}
|
||||
|
||||
export const GameInfo = defineComponent<MainSettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[GameInfoChoice.Statistics, '数据统计'],
|
||||
[GameInfoChoice.Project, '查看工程'],
|
||||
[GameInfoChoice.Tower, '游戏主页'],
|
||||
[GameInfoChoice.Help, '操作帮助'],
|
||||
[GameInfoChoice.Download, '下载离线版本'],
|
||||
[GameInfoChoice.Back, '返回主菜单']
|
||||
];
|
||||
|
||||
const [key, scope] = useKey();
|
||||
key.realize('exit', () => props.controller.close(props.instance));
|
||||
|
||||
const choose = async (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case GameInfoChoice.Statistics: {
|
||||
openStatistics(props.controller);
|
||||
break;
|
||||
}
|
||||
case GameInfoChoice.Project: {
|
||||
if (core.platform.isPC) window.open('editor.html', '_blank');
|
||||
else {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'即将离开本游戏,跳转至工程页面,确认跳转?',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
if (confirm) {
|
||||
window.location.href = 'editor-mobile.html';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameInfoChoice.Tower: {
|
||||
const name = core.firstData.name;
|
||||
const href = `/tower/?name=${name}`;
|
||||
if (core.platform.isPC) {
|
||||
window.open(href, '_blank');
|
||||
} else {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'即将离开本游戏,跳转至评论页面,确认跳转?',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
if (confirm) {
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameInfoChoice.Download: {
|
||||
const name = core.firstData.name;
|
||||
const href = `/games/${name}/${name}.zip`;
|
||||
if (core.platform.isPC) window.open(href);
|
||||
else window.location.href = href;
|
||||
break;
|
||||
}
|
||||
case GameInfoChoice.Help: {
|
||||
// todo
|
||||
break;
|
||||
}
|
||||
case GameInfoChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
choices={choices}
|
||||
width={POP_BOX_WIDTH}
|
||||
onChoose={choose}
|
||||
interval={8}
|
||||
scope={scope}
|
||||
/>
|
||||
);
|
||||
}, mainSettingsProps);
|
||||
|
||||
const enum SyncSaveChoice {
|
||||
// ----- 主菜单
|
||||
ToServer,
|
||||
FromServer,
|
||||
ToLocal,
|
||||
FromLocal,
|
||||
ClearLocal,
|
||||
Back,
|
||||
// ----- 子菜单
|
||||
AllSaves,
|
||||
NowSave
|
||||
}
|
||||
|
||||
export const SyncSave = defineComponent<MainSettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[SyncSaveChoice.ToServer, '同步存档至服务器'],
|
||||
[SyncSaveChoice.FromServer, '从服务器加载存档'],
|
||||
[SyncSaveChoice.ToLocal, '存档至本地文件'],
|
||||
[SyncSaveChoice.FromLocal, '从本地文件读档'],
|
||||
[SyncSaveChoice.ClearLocal, '清空本地存档'],
|
||||
[SyncSaveChoice.Back, '返回上一级']
|
||||
];
|
||||
|
||||
const controller: IUIController<IGameUIPropsBase> = props.controller;
|
||||
const [key, scope] = useKey();
|
||||
key.realize('exit', () => props.controller.close(props.instance));
|
||||
|
||||
const choose = async (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case SyncSaveChoice.ToServer: {
|
||||
controller.open(SyncSaveSelectUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.FromServer: {
|
||||
const replay = await getInput(
|
||||
props.controller,
|
||||
'请输入存档编号+密码',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
await syncFromServer(props.controller, replay);
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.ToLocal: {
|
||||
controller.open(DownloadSaveSelectUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.FromLocal: {
|
||||
// todo
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.ClearLocal: {
|
||||
controller.open(ClearSaveSelectUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
width={POP_BOX_WIDTH}
|
||||
choices={choices}
|
||||
onChoose={choose}
|
||||
interval={8}
|
||||
scope={scope}
|
||||
/>
|
||||
);
|
||||
}, mainSettingsProps);
|
||||
|
||||
export const SyncSaveSelect = defineComponent<MainSettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[SyncSaveChoice.AllSaves, '同步全部存档'],
|
||||
[SyncSaveChoice.NowSave, '同步当前存档'],
|
||||
[SyncSaveChoice.Back, '返回上一级']
|
||||
];
|
||||
|
||||
const [key, scope] = useKey();
|
||||
key.realize('exit', () => props.controller.close(props.instance));
|
||||
|
||||
const choose = async (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case SyncSaveChoice.AllSaves: {
|
||||
props.state.soundPlayer.play('confirm.opus');
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'你确定要同步全部存档么?这可能在存档较多的时候比较慢。',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
if (confirm) {
|
||||
core.syncSave('all');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.NowSave: {
|
||||
props.state.soundPlayer.play('confirm.opus');
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'确定要同步当前存档吗?',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
if (confirm) {
|
||||
core.syncSave();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
width={POP_BOX_WIDTH}
|
||||
choices={choices}
|
||||
onChoose={choose}
|
||||
interval={8}
|
||||
scope={scope}
|
||||
/>
|
||||
);
|
||||
}, mainSettingsProps);
|
||||
|
||||
export const DownloadSaveSelect = defineComponent<MainSettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[SyncSaveChoice.AllSaves, '下载全部存档'],
|
||||
[SyncSaveChoice.NowSave, '下载当前存档'],
|
||||
[SyncSaveChoice.Back, '返回上一级']
|
||||
];
|
||||
|
||||
const [key, scope] = useKey();
|
||||
key.realize('exit', () => props.controller.close(props.instance));
|
||||
|
||||
const choose = async (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case SyncSaveChoice.AllSaves: {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'确认要下载所有存档吗?',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
if (confirm) {
|
||||
const data = await waitbox(
|
||||
props.controller,
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH,
|
||||
getAllSavesData(),
|
||||
{ text: '请等待处理完毕' }
|
||||
);
|
||||
core.download(
|
||||
`${core.firstData.name}_${core.formatDate2(
|
||||
new Date()
|
||||
)}.h5save`,
|
||||
data
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.NowSave: {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'确认要下载当前存档吗?',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
if (confirm) {
|
||||
const data = await getSaveData(core.saves.saveIndex);
|
||||
core.download(
|
||||
`${core.firstData.name}_${core.formatDate2(
|
||||
new Date()
|
||||
)}.h5save`,
|
||||
data
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
width={POP_BOX_WIDTH}
|
||||
choices={choices}
|
||||
onChoose={choose}
|
||||
interval={8}
|
||||
scope={scope}
|
||||
/>
|
||||
);
|
||||
}, mainSettingsProps);
|
||||
|
||||
export const ClearSaveSelect = defineComponent<MainSettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[SyncSaveChoice.AllSaves, '清空全部塔存档'],
|
||||
[SyncSaveChoice.NowSave, '清空当前塔存档'],
|
||||
[SyncSaveChoice.Back, '返回上一级']
|
||||
];
|
||||
|
||||
const [key, scope] = useKey();
|
||||
key.realize('exit', () => props.controller.close(props.instance));
|
||||
|
||||
const choose = async (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case SyncSaveChoice.AllSaves: {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'你确定要清除【全部游戏】的所有本地存档?此行为不可逆!!!',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
if (confirm) {
|
||||
await waitbox(
|
||||
props.controller,
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH,
|
||||
new Promise<void>(res => {
|
||||
core.clearLocalForage(() => {
|
||||
core.saves.ids = {};
|
||||
core.saves.autosave.data = null;
|
||||
core.saves.autosave.updated = false;
|
||||
core.saves.autosave.now = 0;
|
||||
// @ts-expect-error 沙比样板
|
||||
core.saves.cache = {};
|
||||
core.saves.saveIndex = 1;
|
||||
core.saves.favorite = [];
|
||||
core.saves.favoriteName = {};
|
||||
// @ts-expect-error 沙比样板
|
||||
core.control._updateFavoriteSaves();
|
||||
core.removeLocalStorage('saveIndex');
|
||||
res();
|
||||
});
|
||||
}),
|
||||
{ text: '正在情况,请稍后...' }
|
||||
);
|
||||
await getConfirm(
|
||||
props.controller,
|
||||
'所有塔的存档已经全部清空',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.NowSave: {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'你确定要清除【当前游戏】的所有本地存档?此行为不可逆!!!',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
if (confirm) {
|
||||
await waitbox(
|
||||
props.controller,
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH,
|
||||
new Promise<void>(res => {
|
||||
Object.keys(core.saves.ids).forEach(function (v) {
|
||||
core.removeLocalForage('save' + v);
|
||||
});
|
||||
core.removeLocalForage('autoSave', () => {
|
||||
core.saves.ids = {};
|
||||
core.saves.autosave.data = null;
|
||||
core.saves.autosave.updated = false;
|
||||
core.saves.autosave.now = 0;
|
||||
core.ui.closePanel();
|
||||
core.saves.saveIndex = 1;
|
||||
core.saves.favorite = [];
|
||||
core.saves.favoriteName = {};
|
||||
// @ts-expect-error 沙比样板
|
||||
core.control._updateFavoriteSaves();
|
||||
core.removeLocalStorage('saveIndex');
|
||||
res();
|
||||
});
|
||||
}),
|
||||
{ text: '正在情况,请稍后...' }
|
||||
);
|
||||
await getConfirm(
|
||||
props.controller,
|
||||
'当前塔的存档已被清空',
|
||||
CENTER_LOC,
|
||||
POP_BOX_WIDTH
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
width={240}
|
||||
choices={choices}
|
||||
onChoose={choose}
|
||||
interval={8}
|
||||
scope={scope}
|
||||
/>
|
||||
);
|
||||
}, mainSettingsProps);
|
||||
|
||||
/** @see {@link MainSettings} */
|
||||
export const MainSettingsUI = new GameUI('main-settings', MainSettings);
|
||||
/** @see {@link ReplaySettings} */
|
||||
export const ReplaySettingsUI = new GameUI('replay-settings', ReplaySettings);
|
||||
/** @see {@link GameInfo} */
|
||||
export const GameInfoUI = new GameUI('game-info', GameInfo);
|
||||
/** @see {@link SyncSave} */
|
||||
export const SyncSaveUI = new GameUI('sync-save', SyncSave);
|
||||
/** @see {@link SyncSaveSelect} */
|
||||
export const SyncSaveSelectUI = new GameUI('sync-save-select', SyncSaveSelect);
|
||||
/** @see {@link DownloadSaveSelect} */
|
||||
export const DownloadSaveSelectUI = new GameUI(
|
||||
'download-save-select',
|
||||
DownloadSaveSelect
|
||||
);
|
||||
/** @see {@link ClearSaveSelect} */
|
||||
export const ClearSaveSelectUI = new GameUI(
|
||||
'clear-save-select',
|
||||
ClearSaveSelect
|
||||
);
|
||||
|
||||
export function openSettings(
|
||||
controller: IUIController<IGameUIPropsBase>,
|
||||
loc: ElementLocator,
|
||||
props?: MainSettingsProps
|
||||
) {
|
||||
controller.open(MainSettingsUI, {
|
||||
...props,
|
||||
loc
|
||||
});
|
||||
}
|
||||
|
||||
export function openReplay(
|
||||
controller: IUIController<IGameUIPropsBase>,
|
||||
loc: ElementLocator,
|
||||
props?: MainSettingsProps
|
||||
) {
|
||||
controller.open(ReplaySettingsUI, {
|
||||
...props,
|
||||
loc
|
||||
});
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
import {
|
||||
GameUI,
|
||||
IUIController,
|
||||
SetupComponentOptions,
|
||||
IUIDefaultPropsBase
|
||||
} from '@motajs/system';
|
||||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
waitbox,
|
||||
ListPage,
|
||||
TextContent,
|
||||
IGameUIPropsBase
|
||||
} from '@user/client-base';
|
||||
import { DefaultProps } from '@motajs/render-vue';
|
||||
// @ts-expect-error 需要重构
|
||||
import { ItemState } from '@user/data-state';
|
||||
|
||||
// 未完工
|
||||
|
||||
export interface StatisticsDataOneFloor {
|
||||
enemyCount: number;
|
||||
potionCount: number;
|
||||
gemCount: number;
|
||||
potionValue: number;
|
||||
atkValue: number;
|
||||
defValue: number;
|
||||
mdefValue: number;
|
||||
atkGemCount: number;
|
||||
defGemCount: number;
|
||||
mdefGemCount: number;
|
||||
}
|
||||
|
||||
export interface StatisticsDataPotionGem {
|
||||
atkGemCount: number;
|
||||
defGemCount: number;
|
||||
mdefGemCount: number;
|
||||
potionCount: number;
|
||||
}
|
||||
|
||||
export interface StatisticsData {
|
||||
total: StatisticsDataOneFloor;
|
||||
floors: Map<FloorIds, StatisticsDataOneFloor>;
|
||||
potionGem: StatisticsDataPotionGem;
|
||||
}
|
||||
|
||||
export interface StatisticsProps extends IUIDefaultPropsBase, DefaultProps {
|
||||
data: StatisticsData;
|
||||
}
|
||||
|
||||
const statisticsProps = {
|
||||
props: ['data', 'controller', 'instance']
|
||||
} satisfies SetupComponentOptions<StatisticsProps>;
|
||||
|
||||
export const Statistics = defineComponent<StatisticsProps>(props => {
|
||||
const list: [string, string][] = [
|
||||
['total', '总览'],
|
||||
['floor', '楼层'],
|
||||
['enemy', '怪物'],
|
||||
['potion', '血瓶宝石']
|
||||
];
|
||||
|
||||
const close = () => {
|
||||
props.controller.close(props.instance);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<ListPage
|
||||
list={list}
|
||||
selected="total"
|
||||
loc={[180, 0, 480, 480]}
|
||||
height={470}
|
||||
close
|
||||
onClose={close}
|
||||
lineHeight={24}
|
||||
closeLoc={[10, 470, void 0, void 0, 0, 1]}
|
||||
>
|
||||
{{
|
||||
total: () => <TotalStatistics data={props.data} />,
|
||||
floor: () => <FloorStatistics data={props.data} />,
|
||||
enemy: () => <EnemyStatistics data={props.data} />,
|
||||
potion: () => <PotionStatistics data={props.data} />
|
||||
}}
|
||||
</ListPage>
|
||||
);
|
||||
}, statisticsProps);
|
||||
|
||||
interface StatisticsPanelProps extends DefaultProps {
|
||||
data: StatisticsData;
|
||||
}
|
||||
|
||||
const statisticsPanelProps = {
|
||||
props: ['data']
|
||||
} satisfies SetupComponentOptions<StatisticsPanelProps>;
|
||||
|
||||
const TotalStatistics = defineComponent<StatisticsPanelProps>(props => {
|
||||
return () => {
|
||||
const total = props.data.total;
|
||||
|
||||
const text1 = `全塔地图中,共有怪物${total.enemyCount}个。`;
|
||||
const text2 = `共有宝石${total.gemCount}个,共加攻击力${total.atkValue}点、防御力${total.defValue}点,魔防${total.mdefValue}点。`;
|
||||
const text3 = `共有血瓶${total.potionCount}个,共加生命值${total.potionValue}点。`;
|
||||
|
||||
return (
|
||||
<TextContent
|
||||
text={`${text1}${text2}${text3}`}
|
||||
width={330}
|
||||
loc={[0, 5, 330, 470]}
|
||||
height={470}
|
||||
lineHeight={9}
|
||||
></TextContent>
|
||||
);
|
||||
};
|
||||
}, statisticsPanelProps);
|
||||
|
||||
const FloorStatistics = defineComponent<StatisticsPanelProps>(props => {
|
||||
return () => <container></container>;
|
||||
}, statisticsPanelProps);
|
||||
|
||||
const EnemyStatistics = defineComponent<StatisticsPanelProps>(props => {
|
||||
return () => <container></container>;
|
||||
}, statisticsPanelProps);
|
||||
|
||||
const PotionStatistics = defineComponent<StatisticsPanelProps>(props => {
|
||||
return () => {
|
||||
const gemPotion = props.data.potionGem;
|
||||
|
||||
const text1 = `全塔地图中,共有红宝石${gemPotion.atkGemCount}个,共有蓝宝石${gemPotion.defGemCount}个,共有缘宝石${gemPotion.mdefGemCount}个。`;
|
||||
const text2 = `共有血瓶${gemPotion.potionCount}个。`;
|
||||
return (
|
||||
<TextContent
|
||||
text={`${text1}${text2}`}
|
||||
width={330}
|
||||
loc={[0, 5, 330, 470]}
|
||||
height={470}
|
||||
lineHeight={9}
|
||||
></TextContent>
|
||||
);
|
||||
};
|
||||
}, statisticsPanelProps);
|
||||
|
||||
export function calculateStatisticsOne(
|
||||
floorId: FloorIds,
|
||||
diff?: Map<string, number>
|
||||
) {
|
||||
core.setFlag('__statistics__', true);
|
||||
const hasDiff = !!diff;
|
||||
if (!hasDiff) {
|
||||
diff = new Map();
|
||||
const hero = core.status.hero;
|
||||
const handler: ProxyHandler<HeroStatus> = {
|
||||
set(target, p, newValue) {
|
||||
if (typeof p !== 'string') return true;
|
||||
if (typeof newValue === 'number') {
|
||||
const value = diff!.get(p) ?? 0;
|
||||
const delta =
|
||||
newValue - (target[p as keyof HeroStatus] as number);
|
||||
diff!.set(p, value + delta);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
const proxy = new Proxy(hero, handler);
|
||||
core.status.hero = proxy;
|
||||
}
|
||||
core.extractBlocks(floorId);
|
||||
const statistics: StatisticsDataOneFloor = {
|
||||
enemyCount: 0,
|
||||
potionCount: 0,
|
||||
gemCount: 0,
|
||||
potionValue: 0,
|
||||
atkValue: 0,
|
||||
defValue: 0,
|
||||
mdefValue: 0,
|
||||
atkGemCount: 0,
|
||||
defGemCount: 0,
|
||||
mdefGemCount: 0
|
||||
};
|
||||
if (!diff) return statistics;
|
||||
core.status.maps[floorId].blocks.forEach(v => {
|
||||
if (v.event.cls === 'enemys' || v.event.cls === 'enemy48') {
|
||||
statistics.enemyCount++;
|
||||
} else if (v.event.cls === 'items') {
|
||||
const item = ItemState.items.get(v.event.id as AllIdsOf<'items'>);
|
||||
if (!item) return;
|
||||
if (item.cls === 'items') {
|
||||
try {
|
||||
item.itemEffectFn?.();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
const hp = diff.get('hp') ?? 0;
|
||||
const atk = diff.get('atk') ?? 0;
|
||||
const def = diff.get('def') ?? 0;
|
||||
const mdef = diff.get('mdef') ?? 0;
|
||||
if (hp > 0) {
|
||||
statistics.potionCount++;
|
||||
statistics.potionValue += hp;
|
||||
}
|
||||
if (atk > 0 || def > 0 || mdef > 0) {
|
||||
statistics.gemCount++;
|
||||
}
|
||||
if (atk > 0) {
|
||||
statistics.atkValue += atk;
|
||||
statistics.atkGemCount++;
|
||||
}
|
||||
if (def > 0) {
|
||||
statistics.defValue += def;
|
||||
statistics.defGemCount++;
|
||||
}
|
||||
if (mdef > 0) {
|
||||
statistics.mdefValue += mdef;
|
||||
statistics.mdefGemCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
diff.clear();
|
||||
});
|
||||
|
||||
if (!hasDiff) {
|
||||
core.status.hero = hero;
|
||||
window.hero = hero;
|
||||
window.flags = core.status.hero.flags;
|
||||
}
|
||||
core.removeFlag('__statistics__');
|
||||
return statistics;
|
||||
}
|
||||
|
||||
export function calculateStatistics(): StatisticsData {
|
||||
core.setFlag('__statistics__', true);
|
||||
const hero = core.status.hero;
|
||||
const diff = new Map<string, number>();
|
||||
const handler: ProxyHandler<HeroStatus> = {
|
||||
set(target, p, newValue) {
|
||||
if (typeof p !== 'string') return true;
|
||||
if (typeof newValue === 'number') {
|
||||
const value = diff!.get(p) ?? 0;
|
||||
const delta =
|
||||
newValue - (target[p as keyof HeroStatus] as number);
|
||||
diff!.set(p, value + delta);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
const proxy = new Proxy(hero, handler);
|
||||
core.status.hero = proxy;
|
||||
|
||||
const floors = new Map<FloorIds, StatisticsDataOneFloor>();
|
||||
core.floorIds.forEach(v => {
|
||||
const statistics = calculateStatisticsOne(v, diff);
|
||||
floors.set(v, statistics);
|
||||
});
|
||||
|
||||
core.status.hero = hero;
|
||||
window.hero = hero;
|
||||
window.flags = core.status.hero.flags;
|
||||
core.removeFlag('__statistics__');
|
||||
|
||||
const total = floors.values().reduce((prev, curr) => {
|
||||
prev.atkValue += curr.atkValue;
|
||||
prev.defValue += curr.defValue;
|
||||
prev.enemyCount += curr.enemyCount;
|
||||
prev.gemCount += curr.gemCount;
|
||||
prev.mdefValue += curr.mdefValue;
|
||||
prev.potionCount += curr.potionCount;
|
||||
prev.potionValue += curr.potionValue;
|
||||
return prev;
|
||||
});
|
||||
|
||||
const potionGem = floors.values().reduce((prev, curr) => {
|
||||
prev.atkGemCount += curr.atkGemCount;
|
||||
prev.defGemCount += curr.defGemCount;
|
||||
prev.mdefGemCount += curr.mdefGemCount;
|
||||
prev.potionCount += curr.potionCount;
|
||||
return prev;
|
||||
});
|
||||
|
||||
return {
|
||||
total,
|
||||
floors,
|
||||
potionGem
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开数据统计界面
|
||||
* @param controller 要在哪个 UI 控制器上打开
|
||||
*/
|
||||
export async function openStatistics(
|
||||
controller: IUIController<IGameUIPropsBase>
|
||||
) {
|
||||
const cal = Promise.resolve().then<StatisticsData>(() => {
|
||||
return new Promise(res => {
|
||||
const data = calculateStatistics();
|
||||
res(data);
|
||||
});
|
||||
});
|
||||
const data = await waitbox(
|
||||
controller,
|
||||
[240 + 180, 240, void 0, void 0, 0.5, 0.5],
|
||||
240,
|
||||
cal,
|
||||
{
|
||||
text: '正在统计...'
|
||||
}
|
||||
);
|
||||
controller.open(StatisticsUI, { data: data });
|
||||
}
|
||||
|
||||
export const StatisticsUI = new GameUI('statistics', Statistics);
|
||||
@@ -1,557 +0,0 @@
|
||||
import { GameUI, SetupComponentOptions } from '@motajs/system';
|
||||
import {
|
||||
computed,
|
||||
ComputedRef,
|
||||
defineComponent,
|
||||
onUnmounted,
|
||||
reactive,
|
||||
shallowReactive
|
||||
} from 'vue';
|
||||
import { TextContent, IClientBaseExtended } from '@user/client-base';
|
||||
import { ElementLocator, Font, ITexture } from '@motajs/render';
|
||||
import { MixedToolbar, ReplayingStatus } from './toolbar';
|
||||
import { openViewMap } from './viewmap';
|
||||
import { mainUIController } from './controller';
|
||||
import {
|
||||
MAIN_HEIGHT,
|
||||
MAIN_WIDTH,
|
||||
STATUS_BAR_HEIGHT,
|
||||
STATUS_BAR_WIDTH
|
||||
} from '../shared';
|
||||
import { DefaultProps } from '@motajs/render-vue';
|
||||
|
||||
export interface ILeftHeroStatus {
|
||||
/** 楼层 id */
|
||||
floor: FloorIds;
|
||||
/** 等级名称 */
|
||||
lv: string;
|
||||
/** 生命值 */
|
||||
hp: number;
|
||||
/** 生命上限 */
|
||||
hpmax: number;
|
||||
/** 魔力值 */
|
||||
mana: number;
|
||||
/** 魔力上限 */
|
||||
manamax: number;
|
||||
/** 攻击力 */
|
||||
atk: number;
|
||||
/** 防御力 */
|
||||
def: number;
|
||||
/** 魔防(护盾) */
|
||||
mdef: number;
|
||||
/** 金币 */
|
||||
money: number;
|
||||
/** 经验值 */
|
||||
exp: number;
|
||||
/** 距离升级剩余经验 */
|
||||
up: number;
|
||||
/** 黄钥匙数量 */
|
||||
yellowKey: number;
|
||||
/** 蓝钥匙数量 */
|
||||
blueKey: number;
|
||||
/** 红钥匙数量 */
|
||||
redKey: number;
|
||||
/** 绿钥匙数量 */
|
||||
greenKey: number;
|
||||
/** 破数量 */
|
||||
pickaxe: number;
|
||||
/** 炸数量 */
|
||||
bomb: number;
|
||||
/** 飞数量 */
|
||||
centerFly: number;
|
||||
/** 是否中毒 */
|
||||
poison: boolean;
|
||||
/** 是否中衰 */
|
||||
weak: boolean;
|
||||
/** 是否中咒 */
|
||||
curse: boolean;
|
||||
/** 录像状态 */
|
||||
replay: ReplayingStatus;
|
||||
}
|
||||
|
||||
export interface IRightHeroStatus {
|
||||
/** 示例属性,以游戏难度作为示例 */
|
||||
exampleHard: number;
|
||||
}
|
||||
|
||||
interface StatusInfo {
|
||||
/** 图标 */
|
||||
readonly icon: ITexture | null;
|
||||
/** 属性值,经过格式化 */
|
||||
readonly value: ComputedRef<string>;
|
||||
/** 字体 */
|
||||
readonly font: Font;
|
||||
/** 文字颜色 */
|
||||
readonly color: CanvasStyle;
|
||||
}
|
||||
|
||||
interface KeyLikeItem {
|
||||
/** 属性值,经过格式化 */
|
||||
readonly value: ComputedRef<string>;
|
||||
/** 字体 */
|
||||
readonly font: Font;
|
||||
/** 文字颜色 */
|
||||
readonly color: CanvasStyle;
|
||||
}
|
||||
|
||||
interface KeyLikeInfo {
|
||||
/** 这一行包含的内容 */
|
||||
readonly items: KeyLikeItem[];
|
||||
}
|
||||
|
||||
interface StatusBarProps extends DefaultProps, IClientBaseExtended {
|
||||
loc: ElementLocator;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
const statusBarProps = {
|
||||
props: ['loc', 'hidden', 'state']
|
||||
} satisfies SetupComponentOptions<StatusBarProps>;
|
||||
|
||||
export const LeftStatusBar = defineComponent<StatusBarProps>(p => {
|
||||
//#region 参数定义
|
||||
|
||||
/** 属性文字的横坐标 */
|
||||
const TEXT_X = 54;
|
||||
/** 图标的横坐标 */
|
||||
const STATUS_PAD = 8;
|
||||
/** 楼层名称的高度 */
|
||||
const TITLE_HEIGHT = 36;
|
||||
/** 状态属性的开始纵坐标 */
|
||||
const STATUS_Y = TITLE_HEIGHT + STATUS_PAD;
|
||||
|
||||
const materials = p.state.materials;
|
||||
|
||||
// 可以换成 materials.getImageByAlias('xxx.png') 来使用全塔属性注册的图片
|
||||
const hpIcon = materials.getImageByAlias('icon-hp');
|
||||
const atkIcon = materials.getImageByAlias('icon-atk');
|
||||
const defIcon = materials.getImageByAlias('icon-def');
|
||||
const mdefIcon = materials.getImageByAlias('icon-mdef');
|
||||
const moneyIcon = materials.getImageByAlias('icon-money');
|
||||
const expIcon = materials.getImageByAlias('icon-exp');
|
||||
const manaIcon = materials.getImageByAlias('icon-mana');
|
||||
const lvIcon = materials.getImageByAlias('icon-lv');
|
||||
|
||||
const leftStatus: ILeftHeroStatus = reactive({
|
||||
hp: 0,
|
||||
hpmax: 0,
|
||||
mana: 0,
|
||||
manamax: 0,
|
||||
atk: 0,
|
||||
def: 0,
|
||||
mdef: 0,
|
||||
money: 0,
|
||||
exp: 0,
|
||||
// TODO: 缺少「距升级所需经验」钩子(legacy core.getNextLvUpNeed 无新对应)
|
||||
up: 0,
|
||||
// TODO: 缺少道具数量变更钩子(IHeroItems 未实现 IHookable)
|
||||
yellowKey: 0,
|
||||
blueKey: 0,
|
||||
redKey: 0,
|
||||
greenKey: 0,
|
||||
pickaxe: 0,
|
||||
bomb: 0,
|
||||
centerFly: 0,
|
||||
// TODO: flags 更新未接(当前无监听系统)
|
||||
poison: false,
|
||||
weak: false,
|
||||
curse: false,
|
||||
floor: 'MT0',
|
||||
lv: '',
|
||||
// TODO: 缺少聚合录像状态钩子(replaySystem 仅提供沙箱级钩子 onCreateSandbox/onStartReplay 等);原值来源 legacy core.status.replay
|
||||
replay: reactive<ReplayingStatus>({
|
||||
replaying: false,
|
||||
playing: false,
|
||||
speed: 1,
|
||||
played: 0,
|
||||
total: 0
|
||||
})
|
||||
});
|
||||
|
||||
const s = leftStatus;
|
||||
|
||||
const refreshAttribute = () => {
|
||||
s.atk = p.state.hero.attribute.getFinalAttribute('atk');
|
||||
s.hp = p.state.hero.attribute.getFinalAttribute('hp');
|
||||
s.hpmax = p.state.hero.attribute.getFinalAttribute('hpmax');
|
||||
s.mana = p.state.hero.attribute.getFinalAttribute('mana');
|
||||
s.manamax = p.state.hero.attribute.getFinalAttribute('manamax');
|
||||
s.def = p.state.hero.attribute.getFinalAttribute('def');
|
||||
s.mdef = p.state.hero.attribute.getFinalAttribute('mdef');
|
||||
s.money = p.state.hero.attribute.getFinalAttribute('money');
|
||||
s.exp = p.state.hero.attribute.getFinalAttribute('exp');
|
||||
s.lv = core.getLvName(
|
||||
p.state.hero.attribute.getFinalAttribute('level')
|
||||
);
|
||||
};
|
||||
|
||||
const attributeHook = p.state.hero.attribute.addHook({
|
||||
onUpdateAttribute: () => refreshAttribute()
|
||||
});
|
||||
const locationHook = p.state.hero.location.addHook({
|
||||
onSetFloor: map => {
|
||||
if (map) s.floor = core.status.floorId;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
attributeHook.unload();
|
||||
locationHook.unload();
|
||||
});
|
||||
|
||||
/** 常规字体 */
|
||||
const font1 = Font.defaults({ size: 18 });
|
||||
/** 加粗字体 */
|
||||
const font2 = Font.defaults({ size: 18, weight: 700 });
|
||||
|
||||
/** 楼层名 */
|
||||
const floorName = computed(() => core.floors[s.floor]?.title ?? '');
|
||||
|
||||
/** 钥匙显示文字 */
|
||||
const key = (num: number) => {
|
||||
return num.toString().padStart(2, '0');
|
||||
};
|
||||
|
||||
//#region 属性显示
|
||||
|
||||
/** 一般属性 */
|
||||
const statusInfo: StatusInfo[] = shallowReactive([]);
|
||||
/** 钥匙属性 */
|
||||
const keyLike: KeyLikeInfo[] = shallowReactive([]);
|
||||
|
||||
// 根据全塔属性配置显示属性
|
||||
// 如果你想修改状态栏显示,不建议修改这里的内容,而建议直接新增标签
|
||||
// 这里的内容是为使用样板默认状态栏的人准备的,对于新增属性来说过于复杂,而且功能有限,对于新手来说也难以理解
|
||||
// 可以参考说明文档中的常见需求指南中的新增状态教程
|
||||
|
||||
const list = core.flags.statusBarItems;
|
||||
|
||||
// 等级
|
||||
if (list.includes('enableLv')) {
|
||||
statusInfo.push({
|
||||
icon: lvIcon,
|
||||
value: computed(() => s.lv),
|
||||
font: font1,
|
||||
color: '#fff'
|
||||
});
|
||||
}
|
||||
|
||||
// 生命值
|
||||
if (list.includes('enableHP')) {
|
||||
if (list.includes('enableHPMax')) {
|
||||
// 如果启用血限
|
||||
statusInfo.push({
|
||||
icon: hpIcon,
|
||||
value: computed(() => {
|
||||
const hp = core.formatBigNumber(s.hp);
|
||||
const hpmax = core.formatBigNumber(s.hpmax);
|
||||
return `${hp} / ${hpmax}`;
|
||||
}),
|
||||
font: font1,
|
||||
color: '#fff'
|
||||
});
|
||||
} else {
|
||||
// 如果禁用血限
|
||||
statusInfo.push({
|
||||
icon: hpIcon,
|
||||
value: computed(() => core.formatBigNumber(s.hp)),
|
||||
font: font1,
|
||||
color: '#fff'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 魔力
|
||||
if (list.includes('enableMana')) {
|
||||
statusInfo.push({
|
||||
icon: manaIcon,
|
||||
value: computed(() => {
|
||||
const mana = core.formatBigNumber(s.mana);
|
||||
const manamax = core.formatBigNumber(s.manamax);
|
||||
if (s.manamax > 0) {
|
||||
// 如果启用魔力上限
|
||||
return `${mana} / ${manamax}`;
|
||||
} else {
|
||||
// 如果禁用魔力上限
|
||||
return mana;
|
||||
}
|
||||
}),
|
||||
font: font1,
|
||||
color: '#fff'
|
||||
});
|
||||
}
|
||||
|
||||
// 攻击力
|
||||
if (list.includes('enableAtk')) {
|
||||
statusInfo.push({
|
||||
icon: atkIcon,
|
||||
value: computed(() => core.formatBigNumber(s.atk)),
|
||||
font: font1,
|
||||
color: '#fff'
|
||||
});
|
||||
}
|
||||
|
||||
// 防御力
|
||||
if (list.includes('enableDef')) {
|
||||
statusInfo.push({
|
||||
icon: defIcon,
|
||||
value: computed(() => core.formatBigNumber(s.def)),
|
||||
font: font1,
|
||||
color: '#fff'
|
||||
});
|
||||
}
|
||||
|
||||
// 魔防(护盾)
|
||||
if (list.includes('enableMdef')) {
|
||||
statusInfo.push({
|
||||
icon: mdefIcon,
|
||||
value: computed(() => core.formatBigNumber(s.mdef)),
|
||||
font: font1,
|
||||
color: '#fff'
|
||||
});
|
||||
}
|
||||
|
||||
// 金币
|
||||
if (list.includes('enableMoney')) {
|
||||
statusInfo.push({
|
||||
icon: moneyIcon,
|
||||
value: computed(() => core.formatBigNumber(s.money)),
|
||||
font: font1,
|
||||
color: '#fff'
|
||||
});
|
||||
}
|
||||
|
||||
// 经验值
|
||||
if (list.includes('enableExp')) {
|
||||
if (list.includes('enableLevelUp')) {
|
||||
// 升级模式
|
||||
statusInfo.push({
|
||||
icon: expIcon,
|
||||
value: computed(() => core.formatBigNumber(s.up)),
|
||||
font: font1,
|
||||
color: '#fff'
|
||||
});
|
||||
} else {
|
||||
// 非升级模式
|
||||
statusInfo.push({
|
||||
icon: expIcon,
|
||||
value: computed(() => core.formatBigNumber(s.exp)),
|
||||
font: font1,
|
||||
color: '#fff'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 钥匙
|
||||
if (list.includes('enableKeys')) {
|
||||
const keys: KeyLikeItem[] = [];
|
||||
keyLike.push({ items: keys });
|
||||
// 黄钥匙
|
||||
keys.push({
|
||||
value: computed(() => key(s.yellowKey)),
|
||||
font: font2,
|
||||
color: '#fca'
|
||||
});
|
||||
// 蓝钥匙
|
||||
keys.push({
|
||||
value: computed(() => key(s.blueKey)),
|
||||
font: font2,
|
||||
color: '#aad'
|
||||
});
|
||||
// 红钥匙
|
||||
keys.push({
|
||||
value: computed(() => key(s.redKey)),
|
||||
font: font2,
|
||||
color: '#f88'
|
||||
});
|
||||
// 绿钥匙
|
||||
if (list.includes('enableGreenKey')) {
|
||||
keys.push({
|
||||
value: computed(() => key(s.greenKey)),
|
||||
font: font2,
|
||||
color: '#8f8'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 破炸飞
|
||||
if (list.includes('enablePZF')) {
|
||||
const items: KeyLikeItem[] = [];
|
||||
keyLike.push({ items });
|
||||
items.push({
|
||||
value: computed(() => `破 ${s.pickaxe}`),
|
||||
font: font1,
|
||||
color: '#bc6e27'
|
||||
});
|
||||
items.push({
|
||||
value: computed(() => `炸 ${s.bomb}`),
|
||||
font: font1,
|
||||
color: '#fa14b9'
|
||||
});
|
||||
items.push({
|
||||
value: computed(() => `飞 ${s.centerFly}`),
|
||||
font: font1,
|
||||
color: '#8db600'
|
||||
});
|
||||
}
|
||||
|
||||
// 毒衰咒
|
||||
if (list.includes('enableDebuff')) {
|
||||
const debuffs: KeyLikeItem[] = [];
|
||||
keyLike.push({ items: debuffs });
|
||||
debuffs.push({
|
||||
value: computed(() => (s.poison ? '毒' : '')),
|
||||
font: font1,
|
||||
color: '#affca8'
|
||||
});
|
||||
debuffs.push({
|
||||
value: computed(() => (s.weak ? '衰' : '')),
|
||||
font: font1,
|
||||
color: '#feccd0'
|
||||
});
|
||||
debuffs.push({
|
||||
value: computed(() => (s.curse ? '咒' : '')),
|
||||
font: font1,
|
||||
color: '#c2f4e7'
|
||||
});
|
||||
}
|
||||
|
||||
//#region 布局控制
|
||||
|
||||
/** 用于显示状态的高度,高度=状态栏高度-工具栏高度-楼层名高度-填充高度 */
|
||||
const statusHeight =
|
||||
STATUS_BAR_HEIGHT - 113 - TITLE_HEIGHT - STATUS_PAD * 2;
|
||||
|
||||
/** 每一行的高度 */
|
||||
const rowHeight = computed(() => {
|
||||
const length = statusInfo.length + keyLike.length;
|
||||
return statusHeight / length;
|
||||
});
|
||||
/** 钥匙、破炸飞、毒衰咒开始显示的纵坐标 */
|
||||
const keyStart = computed(() => {
|
||||
const statusHeight = statusInfo.length * rowHeight.value;
|
||||
return STATUS_Y + statusHeight;
|
||||
});
|
||||
|
||||
/**
|
||||
* 左右居中布局
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
const central = (y: number): ElementLocator => {
|
||||
const width = p.loc[2] ?? 200;
|
||||
return [width / 2, y, void 0, void 0, 0.5, 0.5];
|
||||
};
|
||||
|
||||
/**
|
||||
* 点击楼层名打开浏览地图
|
||||
*/
|
||||
const viewMap = () => {
|
||||
openViewMap(mainUIController, [0, 0, MAIN_WIDTH, MAIN_HEIGHT]);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container loc={p.loc} hidden={p.hidden}>
|
||||
<text
|
||||
text={floorName.value}
|
||||
loc={central(18)}
|
||||
font={font1}
|
||||
cursor="pointer"
|
||||
onClick={viewMap}
|
||||
></text>
|
||||
<g-line
|
||||
lineWidth={1}
|
||||
strokeStyle="#888"
|
||||
line={[0, TITLE_HEIGHT, STATUS_BAR_WIDTH, TITLE_HEIGHT]}
|
||||
/>
|
||||
{statusInfo
|
||||
.map((v, i) => {
|
||||
const h = rowHeight.value;
|
||||
const y = STATUS_Y + i * h;
|
||||
const cy = y + h / 2;
|
||||
const iconSize = Math.min(32, h * 0.8);
|
||||
const pad = (h - iconSize) / 2;
|
||||
return [
|
||||
<image
|
||||
loc={[STATUS_PAD, y + pad, iconSize, iconSize]}
|
||||
image={v.icon}
|
||||
noanti // 取消抗锯齿
|
||||
/>,
|
||||
<text
|
||||
loc={[TEXT_X, cy]}
|
||||
anc={[0, 0.5]}
|
||||
text={v.value.value}
|
||||
fillStyle={v.color}
|
||||
font={v.font}
|
||||
/>
|
||||
];
|
||||
})
|
||||
.flat()}
|
||||
{keyLike
|
||||
.map(({ items }, i) => {
|
||||
const h = rowHeight.value;
|
||||
const y = keyStart.value + i * h;
|
||||
const cy = y + h / 2;
|
||||
const rw = STATUS_BAR_WIDTH / (items.length + 1);
|
||||
return items.map((v, i) => {
|
||||
const x = rw * (i + 1);
|
||||
return (
|
||||
<text
|
||||
loc={[x, cy]}
|
||||
anc={[0.5, 0.5]}
|
||||
text={v.value.value}
|
||||
font={v.font}
|
||||
fillStyle={v.color}
|
||||
/>
|
||||
);
|
||||
});
|
||||
})
|
||||
.flat()}
|
||||
<g-line
|
||||
lineWidth={1}
|
||||
strokeStyle="#888"
|
||||
line={[
|
||||
0,
|
||||
MAIN_HEIGHT - 113,
|
||||
STATUS_BAR_WIDTH,
|
||||
MAIN_HEIGHT - 113
|
||||
]}
|
||||
/>
|
||||
<MixedToolbar
|
||||
loc={[0, MAIN_HEIGHT - 113, STATUS_BAR_WIDTH, 113]}
|
||||
status={s.replay}
|
||||
state={p.state}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
}, statusBarProps);
|
||||
|
||||
export const RightStatusBar = defineComponent<StatusBarProps>(p => {
|
||||
// p.status 就是你在 main.tsx 中传入的属性内容,用法与左侧状态栏完全一致
|
||||
|
||||
// TODO: flags 更新未接(当前无监听系统)
|
||||
const rightStatus: IRightHeroStatus = reactive({ exampleHard: 0 });
|
||||
|
||||
const text = `这里是右侧状态栏,如果左侧状态栏不够用可以在 \\r[gold]statusBar.tsx\\r 中编写内容,如果不需要此状态栏,可以在 \\r[gold]shared.ts\\r 中关闭此状态栏。`;
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<container loc={p.loc} hidden={p.hidden}>
|
||||
<TextContent
|
||||
loc={[8, 8]}
|
||||
text={text}
|
||||
width={STATUS_BAR_WIDTH - 16}
|
||||
autoHeight
|
||||
lineHeight={8}
|
||||
/>
|
||||
<text loc={[8, 270]} text="示例内容" />
|
||||
<text
|
||||
loc={[8, 300]}
|
||||
text={`游戏难度:${rightStatus.exampleHard}`}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
};
|
||||
}, statusBarProps);
|
||||
|
||||
export const leftStatusBarUI = new GameUI('left-status-bar', LeftStatusBar);
|
||||
export const rightStatusBarUI = new GameUI('right-status-bar', RightStatusBar);
|
||||
@@ -1,480 +0,0 @@
|
||||
import { DefaultProps } from '@motajs/render-vue';
|
||||
import { GameUI, SetupComponentOptions } from '@motajs/system';
|
||||
import { defineComponent, nextTick, onMounted, ref } from 'vue';
|
||||
import {
|
||||
BUTTONS_HEIGHT,
|
||||
BUTTONS_WIDTH,
|
||||
BUTTONS_X,
|
||||
BUTTONS_Y,
|
||||
HALF_HEIGHT,
|
||||
HALF_WIDTH,
|
||||
MAIN_HEIGHT,
|
||||
MAIN_WIDTH,
|
||||
TITLE_BACKGROUND_IMAGE,
|
||||
TITLE_FILL,
|
||||
TITLE_STROKE,
|
||||
TITLE_STROKE_WIDTH,
|
||||
TITLE_X,
|
||||
TITLE_Y
|
||||
} from '../shared';
|
||||
import { ElementLocator, Font } from '@motajs/render';
|
||||
import {
|
||||
ITransitionedController,
|
||||
transitioned,
|
||||
transitionedColor,
|
||||
useKey
|
||||
} from '../render/use';
|
||||
import { ExitFullscreen, Fullscreen, SoundVolume } from '@user/client-base';
|
||||
import { mainSetting, triggerFullscreen } from '@motajs/legacy-ui';
|
||||
import { saveLoad } from './save';
|
||||
import { MainSceneUI } from './main';
|
||||
import { adjustCover } from '@user/client-base';
|
||||
import { cosh, CurveMode, linear } from '@motajs/animate';
|
||||
import { sleep } from '@motajs/common';
|
||||
import { IUIPropsBase } from './types';
|
||||
|
||||
const enum TitleButton {
|
||||
StartGame,
|
||||
LoadGame,
|
||||
Replay
|
||||
}
|
||||
|
||||
interface ButtonItem {
|
||||
code: TitleButton;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface ButtonOption {
|
||||
code: number;
|
||||
color: string;
|
||||
name: string;
|
||||
hard: string;
|
||||
colorTrans: ITransitionedController<string>;
|
||||
}
|
||||
|
||||
export interface GameTitleProps extends DefaultProps, IUIPropsBase {}
|
||||
|
||||
const gameTitleProps = {
|
||||
props: ['controller', 'instance', 'state']
|
||||
} satisfies SetupComponentOptions<GameTitleProps>;
|
||||
|
||||
export const GameTitle = defineComponent<GameTitleProps>(props => {
|
||||
const materials = props.state.materials;
|
||||
const bg = materials.getImageByAlias(TITLE_BACKGROUND_IMAGE);
|
||||
|
||||
//#region 计算背景图
|
||||
const [width, height] = adjustCover(
|
||||
bg?.width ?? MAIN_WIDTH,
|
||||
bg?.height ?? MAIN_HEIGHT,
|
||||
MAIN_WIDTH,
|
||||
MAIN_HEIGHT
|
||||
);
|
||||
|
||||
//#region 标题设置
|
||||
|
||||
/** 当前是否全屏 */
|
||||
const fullscreen = ref(!!document.fullscreenElement);
|
||||
/** 是否开启了声音 */
|
||||
const soundOpened = ref(true);
|
||||
/** 是否在选择难度界面 */
|
||||
const selectHard = ref(false);
|
||||
|
||||
/** 开始界面按钮定义,你可以在这新增自己的按钮 */
|
||||
const buttonItems: ButtonItem[] = [
|
||||
{
|
||||
code: TitleButton.StartGame,
|
||||
color: 'rgb(40, 194, 255)',
|
||||
name: '开始游戏'
|
||||
},
|
||||
{
|
||||
code: TitleButton.LoadGame,
|
||||
color: 'rgb(0, 255, 55)',
|
||||
name: '读取存档'
|
||||
},
|
||||
{
|
||||
code: TitleButton.Replay,
|
||||
color: 'rgb(255, 251, 0)',
|
||||
name: '录像回放'
|
||||
}
|
||||
];
|
||||
|
||||
/** 开始界面按钮 */
|
||||
const buttons = buttonItems.map<ButtonOption>(v => {
|
||||
return {
|
||||
code: v.code,
|
||||
color: v.color,
|
||||
name: v.name,
|
||||
hard: '',
|
||||
colorTrans: transitionedColor(
|
||||
'#fff',
|
||||
400,
|
||||
cosh(2, CurveMode.EaseOut)
|
||||
)!,
|
||||
scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))!
|
||||
};
|
||||
});
|
||||
|
||||
/** 选择难度界面按钮 */
|
||||
const hard = main.levelChoose.map<ButtonOption>(v => {
|
||||
return {
|
||||
code: v.hard,
|
||||
color: core.arrayToRGBA(v.color!),
|
||||
name: v.title,
|
||||
hard: v.name,
|
||||
colorTrans: transitionedColor(
|
||||
'#fff',
|
||||
400,
|
||||
cosh(2, CurveMode.EaseOut)
|
||||
)!,
|
||||
scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))!
|
||||
};
|
||||
});
|
||||
// 返回按钮
|
||||
hard.push({
|
||||
code: -1,
|
||||
color: '#aaa',
|
||||
name: '返回',
|
||||
hard: '',
|
||||
colorTrans: transitionedColor('#fff', 400, cosh(2, CurveMode.EaseOut))!
|
||||
});
|
||||
|
||||
/** 声音设置按钮的颜色 */
|
||||
const soundColor = transitionedColor(
|
||||
'#ddd',
|
||||
400,
|
||||
cosh(2, CurveMode.EaseOut)
|
||||
)!;
|
||||
|
||||
/** 开始界面按钮的不透明度,选择难度界面的不透明度使用 `1-buttonsAlpha` 计算 */
|
||||
const buttonsAlpha = transitioned(1, 300, linear())!;
|
||||
/** 开始界面的不透明度 */
|
||||
const mainAlpha = transitioned(0, 600, linear())!;
|
||||
|
||||
const buttonFilter = `
|
||||
drop-shadow(3px 3px 5px rgba(0, 0, 0, 0.4))
|
||||
drop-shadow(0px 0px 2px rgba(255, 255, 255, 0.5))
|
||||
`;
|
||||
|
||||
const titleFont = Font.defaults({ size: 72 });
|
||||
const buttonFont = Font.defaults({ size: 24, weight: 600 });
|
||||
|
||||
/** 开始界面按钮的高度 */
|
||||
const buttonHeight = (buttons.length - 1) * 40 + 60;
|
||||
/** 选择难度界面按钮的高度 */
|
||||
const hardHeight = (hard.length - 1) * 40 + 60;
|
||||
/** 按钮的背景框高度 */
|
||||
const rectHeight = transitioned(
|
||||
buttonHeight,
|
||||
600,
|
||||
cosh(2, CurveMode.EaseOut)
|
||||
)!;
|
||||
|
||||
//#region 按钮功能
|
||||
|
||||
/**
|
||||
* 在开始界面和选择难度界面切换
|
||||
*/
|
||||
const toggleHard = async () => {
|
||||
if (selectHard.value) {
|
||||
// 当前在难度界面,将要切换至开始界面
|
||||
enterMain(0);
|
||||
rectHeight.set(buttonHeight);
|
||||
} else {
|
||||
// 当前在开始界面,将要切换至难度界面
|
||||
enterHard(0);
|
||||
rectHeight.set(hardHeight);
|
||||
}
|
||||
buttonsAlpha.set(0);
|
||||
await sleep(300);
|
||||
selectHard.value = !selectHard.value;
|
||||
buttonsAlpha.set(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* 点击读取存档按钮
|
||||
*/
|
||||
const loadGame = async () => {
|
||||
const loc: ElementLocator = [0, 0, MAIN_WIDTH, MAIN_HEIGHT];
|
||||
const success = await saveLoad(props.controller, loc);
|
||||
if (success) {
|
||||
props.controller.close(props.instance);
|
||||
props.controller.open(MainSceneUI, {});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 点击录像回放按钮
|
||||
*/
|
||||
const replay = () => {
|
||||
core.chooseReplayFile();
|
||||
};
|
||||
|
||||
/**
|
||||
* 选择难度并开始游戏
|
||||
* @param hard 选择的难度
|
||||
*/
|
||||
const startGame = async (hard: string) => {
|
||||
mainAlpha.set(0);
|
||||
await sleep(600);
|
||||
props.controller.close(props.instance);
|
||||
props.controller.open(MainSceneUI, {});
|
||||
nextTick(() => {
|
||||
core.startGame(hard);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 点击按钮时触发
|
||||
* @param code 选中的按钮的代码
|
||||
*/
|
||||
const clickButton = (code: number, index: number) => {
|
||||
if (selectHard.value) {
|
||||
if (index === hard.length - 1) {
|
||||
// 选中了最后一个按钮
|
||||
toggleHard();
|
||||
return;
|
||||
}
|
||||
const item = hard[index];
|
||||
startGame(item.name);
|
||||
} else {
|
||||
switch (code) {
|
||||
case TitleButton.StartGame:
|
||||
toggleHard();
|
||||
break;
|
||||
case TitleButton.LoadGame:
|
||||
loadGame();
|
||||
break;
|
||||
case TitleButton.Replay:
|
||||
replay();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//#region 键盘操作
|
||||
|
||||
const selected = ref(0);
|
||||
|
||||
const [key] = useKey();
|
||||
key.realize(
|
||||
'@start_up',
|
||||
() => {
|
||||
selected.value--;
|
||||
if (selected.value < 0) {
|
||||
selected.value = 0;
|
||||
}
|
||||
if (selectHard.value) {
|
||||
enterHard(selected.value);
|
||||
} else {
|
||||
enterMain(selected.value);
|
||||
}
|
||||
},
|
||||
{ type: 'down' }
|
||||
)
|
||||
.realize(
|
||||
'@start_down',
|
||||
() => {
|
||||
selected.value++;
|
||||
if (selectHard.value) {
|
||||
if (selected.value > hard.length - 1) {
|
||||
selected.value = hard.length - 1;
|
||||
}
|
||||
enterHard(selected.value);
|
||||
} else {
|
||||
if (selected.value > buttons.length - 1) {
|
||||
selected.value = buttons.length - 1;
|
||||
}
|
||||
enterMain(selected.value);
|
||||
}
|
||||
},
|
||||
{ type: 'down' }
|
||||
)
|
||||
.realize('confirm', () => {
|
||||
if (selectHard.value) {
|
||||
clickButton(hard[selected.value].code, selected.value);
|
||||
} else {
|
||||
clickButton(buttons[selected.value].code, selected.value);
|
||||
}
|
||||
});
|
||||
|
||||
//#region 鼠标操作
|
||||
|
||||
soundOpened.value = mainSetting.getValue('audio.bgmEnabled', true);
|
||||
if (soundOpened.value) {
|
||||
soundColor.set('#ddd');
|
||||
} else {
|
||||
soundColor.set('#d22');
|
||||
}
|
||||
|
||||
const enterMain = (index: number) => {
|
||||
buttons.forEach((v, i) => {
|
||||
if (index !== i) {
|
||||
v.colorTrans.set('#fff');
|
||||
} else {
|
||||
v.colorTrans.set(v.color);
|
||||
}
|
||||
});
|
||||
selected.value = index;
|
||||
};
|
||||
|
||||
const enterHard = (index: number) => {
|
||||
hard.forEach((v, i) => {
|
||||
if (index !== i) {
|
||||
v.colorTrans.set('#fff');
|
||||
} else {
|
||||
v.colorTrans.set(v.color);
|
||||
}
|
||||
});
|
||||
selected.value = index;
|
||||
};
|
||||
|
||||
const toggleSound = () => {
|
||||
soundOpened.value = !soundOpened.value;
|
||||
mainSetting.setValue('audio.bgmEnabled', soundOpened.value);
|
||||
if (soundOpened.value) {
|
||||
soundColor.set('#ddd');
|
||||
} else {
|
||||
soundColor.set('#d22');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
await triggerFullscreen(!fullscreen.value);
|
||||
fullscreen.value = !!document.fullscreenElement;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
enterMain(0);
|
||||
mainAlpha.set(1);
|
||||
});
|
||||
|
||||
return () => (
|
||||
<container
|
||||
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
|
||||
alpha={mainAlpha.ref.value}
|
||||
>
|
||||
<image
|
||||
image={bg}
|
||||
loc={[HALF_WIDTH, HALF_HEIGHT, width, height]}
|
||||
anc={[0.5, 0.5]}
|
||||
zIndex={0}
|
||||
/>
|
||||
<text
|
||||
text={core.firstData.title}
|
||||
loc={[TITLE_X, TITLE_Y]}
|
||||
anc={[0.5, 0.5]}
|
||||
fillStyle={TITLE_FILL}
|
||||
strokeStyle={TITLE_STROKE}
|
||||
font={titleFont}
|
||||
strokeWidth={TITLE_STROKE_WIDTH}
|
||||
/>
|
||||
<container
|
||||
zIndex={15}
|
||||
loc={[BUTTONS_X, BUTTONS_Y, BUTTONS_WIDTH, BUTTONS_HEIGHT]}
|
||||
anc={[0.5, 0]}
|
||||
>
|
||||
<g-rectr
|
||||
loc={[2, BUTTONS_HEIGHT / 2]}
|
||||
width={BUTTONS_WIDTH - 4}
|
||||
height={rectHeight.ref.value - 4}
|
||||
anc={[0, 0.5]}
|
||||
circle={[16]}
|
||||
fill
|
||||
stroke
|
||||
fillStyle="rgba(50, 54, 159, 0.7)"
|
||||
strokeStyle="rgba(255, 204, 0, 1)"
|
||||
lineWidth={3}
|
||||
zIndex={0}
|
||||
/>
|
||||
<container
|
||||
hidden={selectHard.value}
|
||||
loc={[0, BUTTONS_HEIGHT / 2, BUTTONS_WIDTH, buttonHeight]}
|
||||
anc={[0, 0.5]}
|
||||
alpha={buttonsAlpha.ref.value}
|
||||
zIndex={5}
|
||||
>
|
||||
{buttons.map((v, i) => {
|
||||
const x = BUTTONS_WIDTH / 2;
|
||||
const y = 30 + i * 40;
|
||||
return (
|
||||
<text
|
||||
text={v.name}
|
||||
font={buttonFont}
|
||||
loc={[x, y, void 0, void 0, 0.5, 0.5]}
|
||||
cursor="pointer"
|
||||
filter={buttonFilter}
|
||||
fillStyle={v.colorTrans.ref.value}
|
||||
onEnter={() => enterMain(i)}
|
||||
onClick={() => clickButton(v.code, i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</container>
|
||||
<container
|
||||
hidden={!selectHard.value}
|
||||
loc={[0, BUTTONS_HEIGHT / 2, BUTTONS_WIDTH, hardHeight]}
|
||||
anc={[0, 0.5]}
|
||||
alpha={buttonsAlpha.ref.value}
|
||||
>
|
||||
{hard.map((v, i) => {
|
||||
const x = BUTTONS_WIDTH / 2;
|
||||
const y = 30 + i * 40;
|
||||
return (
|
||||
<text
|
||||
text={v.name}
|
||||
font={buttonFont}
|
||||
loc={[x, y, void 0, void 0, 0.5, 0.5]}
|
||||
cursor="pointer"
|
||||
filter={buttonFilter}
|
||||
fillStyle={v.colorTrans.ref.value}
|
||||
onEnter={() => enterHard(i)}
|
||||
onClick={() => clickButton(v.code, i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</container>
|
||||
</container>
|
||||
<container
|
||||
zIndex={15}
|
||||
loc={[MAIN_WIDTH - 20, MAIN_HEIGHT - 20, 120, 40, 1, 1]}
|
||||
>
|
||||
<g-rectr
|
||||
loc={[0, 0, 120, 40]}
|
||||
circle={[20]}
|
||||
fillStyle="rgba(0, 0, 0, 0.72)"
|
||||
/>
|
||||
<SoundVolume
|
||||
loc={[20, 0, 40, 40]}
|
||||
cursor="pointer"
|
||||
strokeStyle={soundColor.ref.value}
|
||||
onClick={toggleSound}
|
||||
/>
|
||||
{!fullscreen.value ? (
|
||||
<Fullscreen
|
||||
loc={[60, 0, 40, 40]}
|
||||
onClick={toggleFullscreen}
|
||||
cursor="pointer"
|
||||
/>
|
||||
) : (
|
||||
<ExitFullscreen
|
||||
loc={[60, 0, 40, 40]}
|
||||
onClick={toggleFullscreen}
|
||||
cursor="pointer"
|
||||
/>
|
||||
)}
|
||||
<g-line
|
||||
line={[25, 35, 55, 5]}
|
||||
strokeStyle="gray"
|
||||
lineWidth={3}
|
||||
lineCap="round"
|
||||
zIndex={5}
|
||||
noevent
|
||||
hidden={soundOpened.value}
|
||||
/>
|
||||
</container>
|
||||
</container>
|
||||
);
|
||||
}, gameTitleProps);
|
||||
|
||||
export const GameTitleUI = new GameUI('game-title', GameTitle);
|
||||
@@ -1,414 +0,0 @@
|
||||
import { ElementLocator, Font } from '@motajs/render';
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import {
|
||||
DoubleArrow,
|
||||
NumpadIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
ReplayIcon,
|
||||
RetweetIcon,
|
||||
RollbackIcon,
|
||||
StepForward,
|
||||
ViewMapIcon
|
||||
} from '@user/client-base';
|
||||
import { getVitualKeyOnce } from '@motajs/legacy-ui';
|
||||
import { gameKey, generateKeyboardEvent } from '@motajs/system';
|
||||
import { transitioned } from '../render/use';
|
||||
import { linear } from 'mutate-animate';
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
import { IClientBaseExtended, Progress } from '@user/client-base';
|
||||
import { generateBinary } from '@motajs/legacy-common';
|
||||
import { SetupComponentOptions } from '@motajs/system';
|
||||
import { saveSave, saveLoad } from './save';
|
||||
import { mainUIController } from './controller';
|
||||
import { MAIN_HEIGHT, FULL_LOC, POP_BOX_WIDTH, CENTER_LOC } from '../shared';
|
||||
import { openReplay, openSettings } from './settings';
|
||||
import { openViewMap } from './viewmap';
|
||||
import { DefaultProps } from '@motajs/render-vue';
|
||||
|
||||
interface ToolbarProps extends DefaultProps, IClientBaseExtended {
|
||||
loc?: ElementLocator;
|
||||
}
|
||||
|
||||
type ToolbarEmits = {
|
||||
numpad: () => void;
|
||||
};
|
||||
|
||||
const toolbarProps = {
|
||||
props: ['loc', 'state'],
|
||||
emits: ['numpad']
|
||||
} satisfies SetupComponentOptions<
|
||||
ToolbarProps,
|
||||
ToolbarEmits,
|
||||
keyof ToolbarEmits
|
||||
>;
|
||||
|
||||
const im = (col: number, row: number): ElementLocator => {
|
||||
return [5 + 34 * col, 5 + 36 * row, 32, 32];
|
||||
};
|
||||
|
||||
const ic = (col: number, row: number): ElementLocator => {
|
||||
return [7 + 34 * col, 7 + 36 * row, 28, 28];
|
||||
};
|
||||
|
||||
const ic2 = (col: number, row: number): ElementLocator => {
|
||||
return [9 + 34 * col, 9 + 36 * row, 24, 24];
|
||||
};
|
||||
|
||||
const middle = (col: number, row: number): ElementLocator => {
|
||||
return [21 + 34 * col, 21 + 36 * row, void 0, void 0, 0.5, 0.5];
|
||||
};
|
||||
|
||||
const middle2 = (
|
||||
col: number,
|
||||
row: number,
|
||||
width: number,
|
||||
height: number
|
||||
): ElementLocator => {
|
||||
return [21 + 34 * col, 21 + 36 * row, width, height, 0.5, 0.5];
|
||||
};
|
||||
|
||||
export const PlayingToolbar = defineComponent<
|
||||
ToolbarProps,
|
||||
ToolbarEmits,
|
||||
keyof ToolbarEmits
|
||||
>((props, { emit }) => {
|
||||
const materials = props.state.materials;
|
||||
const bookIcon = materials.getImageByAlias('icon-book');
|
||||
const flyIcon = materials.getImageByAlias('icon-fly');
|
||||
const toolIcon = materials.getImageByAlias('icon-toolbox');
|
||||
const equipIcon = materials.getImageByAlias('icon-equipbox');
|
||||
const keyIcon = materials.getImageByAlias('icon-keyboard');
|
||||
const shopIcon = materials.getImageByAlias('icon-shop');
|
||||
const saveIcon = materials.getImageByAlias('icon-save');
|
||||
const loadIcon = materials.getImageByAlias('icon-load');
|
||||
const setIcon = materials.getImageByAlias('icon-settings');
|
||||
|
||||
const iconFont = new Font('Verdana', 12);
|
||||
|
||||
const book = () => core.openBook(true);
|
||||
const tool = () => core.openToolbox(true);
|
||||
const fly = () => core.useFly(true);
|
||||
const save = () => {
|
||||
saveSave(mainUIController, FULL_LOC);
|
||||
};
|
||||
const load = () => {
|
||||
saveLoad(mainUIController, FULL_LOC);
|
||||
};
|
||||
const equip = () => core.openEquipbox(true);
|
||||
const shop = () => core.openQuickShop(true);
|
||||
const key = () => {
|
||||
getVitualKeyOnce().then(value => {
|
||||
gameKey.emitKey(
|
||||
value.key,
|
||||
value.assist,
|
||||
'up',
|
||||
generateKeyboardEvent(value.key, value.assist)
|
||||
);
|
||||
});
|
||||
};
|
||||
const undo = () => core.doSL('autoSave', 'load');
|
||||
const redo = () => core.doSL('autoSave', 'reload');
|
||||
const numpad = () => emit('numpad');
|
||||
const view = () => {
|
||||
openViewMap(mainUIController, FULL_LOC);
|
||||
};
|
||||
const replay = () => {
|
||||
const loc = CENTER_LOC.slice() as ElementLocator;
|
||||
loc[2] = POP_BOX_WIDTH;
|
||||
openReplay(mainUIController, loc);
|
||||
};
|
||||
const settings = () => {
|
||||
const loc = CENTER_LOC.slice() as ElementLocator;
|
||||
loc[2] = POP_BOX_WIDTH;
|
||||
loc[3] = MAIN_HEIGHT - 72;
|
||||
openSettings(mainUIController, loc);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container loc={props.loc} cursor="pointer">
|
||||
<image image={bookIcon} loc={im(0, 0)} noanti onClick={book} />
|
||||
<image image={toolIcon} loc={im(1, 0)} noanti onClick={tool} />
|
||||
<image image={flyIcon} loc={im(2, 0)} noanti onClick={fly} />
|
||||
<image image={saveIcon} loc={im(3, 0)} noanti onClick={save} />
|
||||
<image image={loadIcon} loc={im(4, 0)} noanti onClick={load} />
|
||||
<image image={equipIcon} loc={im(0, 1)} noanti onClick={equip} />
|
||||
<image image={shopIcon} loc={im(1, 1)} noanti onClick={shop} />
|
||||
<image image={keyIcon} loc={im(2, 1)} noanti onClick={key} />
|
||||
<RollbackIcon loc={ic(3, 1)} strokeStyle="#eee" onClick={undo} />
|
||||
<RetweetIcon loc={ic(4, 1)} strokeStyle="#eee" onClick={redo} />
|
||||
<NumpadIcon loc={ic(0, 2)} strokeStyle="#eee" onClick={numpad} />
|
||||
<ViewMapIcon loc={ic(1, 2)} strokeStyle="#eee" onClick={view} />
|
||||
<ReplayIcon loc={ic(3, 2)} strokeStyle="#eee" onClick={replay} />
|
||||
<text text="R" loc={middle(3, 2)} font={iconFont} noevent />
|
||||
<image image={setIcon} loc={im(4, 2)} noanti onClick={settings} />
|
||||
</container>
|
||||
);
|
||||
}, toolbarProps);
|
||||
|
||||
export interface ReplayingStatus {
|
||||
/** 是否处在录像播放状态 */
|
||||
replaying: boolean;
|
||||
/** 是否正在播放 */
|
||||
playing: boolean;
|
||||
/** 录像播放速度 */
|
||||
speed: number;
|
||||
/** 已播放的长度 */
|
||||
played: number;
|
||||
/** 总长度 */
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ReplayingProps extends ToolbarProps {
|
||||
/** 录像播放状态 */
|
||||
status: ReplayingStatus;
|
||||
}
|
||||
|
||||
const replayingProps = {
|
||||
props: ['status', 'loc', 'state']
|
||||
} satisfies SetupComponentOptions<ReplayingProps>;
|
||||
|
||||
export const ReplayingToolbar = defineComponent<ReplayingProps>(props => {
|
||||
const status = props.status;
|
||||
|
||||
const materials = props.state.materials;
|
||||
const bookIcon = materials.getImageByAlias('icon-book');
|
||||
const saveIcon = materials.getImageByAlias('icon-save');
|
||||
const font1 = Font.defaults({ size: 16 });
|
||||
const font2 = new Font('Verdana', 12);
|
||||
|
||||
const speedText = computed(() => `${status.speed}速`);
|
||||
const progress = computed(() => status.played / status.total);
|
||||
const progressText1 = computed(() => `${status.played}/${status.total}`);
|
||||
const progressText2 = computed(
|
||||
() => `${(progress.value * 100).toFixed(2)}%`
|
||||
);
|
||||
|
||||
const play = () => core.resumeReplay();
|
||||
const pause = () => core.pauseReplay();
|
||||
const stop = () => core.stopReplay(true);
|
||||
const speedDown = () => core.speedDownReplay();
|
||||
const speedUp = () => core.speedUpReplay();
|
||||
const book = () => core.openBook(true);
|
||||
const save = () => {
|
||||
saveSave(mainUIController, FULL_LOC);
|
||||
};
|
||||
const view = () => {
|
||||
if (core.isPlaying() && !core.isMoving() && !core.status.lockControl) {
|
||||
core.ui._drawViewMaps();
|
||||
}
|
||||
};
|
||||
const rewind = () => core.rewindReplay();
|
||||
const step = () => core.stepReplay();
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<container loc={props.loc} cursor="pointer">
|
||||
{status.playing ? (
|
||||
<PauseIcon loc={ic(0, 0)} onClick={pause} />
|
||||
) : (
|
||||
<PlayIcon loc={ic(0, 0)} onClick={play} />
|
||||
)}
|
||||
<g-rectr loc={[47, 13, 16, 16]} circle={[2]} onClick={stop} />
|
||||
<DoubleArrow
|
||||
loc={middle2(2, 0, 28, 28)}
|
||||
scale={[-1, 1]}
|
||||
onClick={speedDown}
|
||||
/>
|
||||
<text text={speedText.value} loc={middle(3, 0)} font={font1} />
|
||||
<DoubleArrow loc={ic(4, 0)} onClick={speedUp} />
|
||||
<image image={bookIcon} loc={im(0, 1)} noanti onClick={book} />
|
||||
<image image={saveIcon} loc={im(1, 1)} noanti onClick={save} />
|
||||
<ViewMapIcon loc={ic(2, 1)} onClick={view} />
|
||||
<StepForward
|
||||
loc={middle2(3, 1, 28, 28)}
|
||||
scale={[-1, 1]}
|
||||
onClick={rewind}
|
||||
/>
|
||||
<StepForward loc={ic(4, 1)} onClick={step} />
|
||||
<text
|
||||
text={progressText1.value}
|
||||
loc={[12, 98, void 0, void 0, 0, 1]}
|
||||
font={font2}
|
||||
/>
|
||||
<text
|
||||
text={progressText2.value}
|
||||
loc={[168, 98, void 0, void 0, 1, 1]}
|
||||
font={font2}
|
||||
/>
|
||||
<Progress
|
||||
loc={[12, 101, 156, 4]}
|
||||
progress={progress.value}
|
||||
success="lightgreen"
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
};
|
||||
}, replayingProps);
|
||||
|
||||
export const NumpadToolbar = defineComponent<
|
||||
ToolbarProps,
|
||||
ToolbarEmits,
|
||||
keyof ToolbarEmits
|
||||
>((props, { emit }) => {
|
||||
const numpad = () => emit('numpad');
|
||||
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
|
||||
const font = new Font('Verdana', 14);
|
||||
|
||||
const ctrlEnabled = ref(false);
|
||||
const shiftEnabled = ref(false);
|
||||
const altEnabled = ref(false);
|
||||
|
||||
const ctrlAlpha = transitioned(0, 100, linear())!;
|
||||
const shiftAlpha = transitioned(0, 100, linear())!;
|
||||
const altAlpha = transitioned(0, 100, linear())!;
|
||||
|
||||
const ctrlColor = computed(
|
||||
() => `rgba(221,221,221,${ctrlAlpha.ref.value})`
|
||||
);
|
||||
const ctrlTextColor = computed(() => {
|
||||
const rgb = Math.floor(255 - ctrlAlpha.ref.value * 255);
|
||||
return `rgba(${rgb},${rgb},${rgb},1)`;
|
||||
});
|
||||
|
||||
const shiftColor = computed(
|
||||
() => `rgba(221,221,221,${shiftAlpha.ref.value})`
|
||||
);
|
||||
const shiftTextColor = computed(() => {
|
||||
const rgb = Math.floor(255 - shiftAlpha.ref.value * 255);
|
||||
return `rgba(${rgb},${rgb},${rgb},1)`;
|
||||
});
|
||||
|
||||
const altColor = computed(() => `rgba(221,221,221,${altAlpha.ref.value})`);
|
||||
const altTextColor = computed(() => {
|
||||
const rgb = Math.floor(255 - altAlpha.ref.value * 255);
|
||||
return `rgba(${rgb},${rgb},${rgb},1)`;
|
||||
});
|
||||
|
||||
const clickCtrl = () => {
|
||||
ctrlEnabled.value = !ctrlEnabled.value;
|
||||
ctrlAlpha.set(ctrlEnabled.value ? 1 : 0);
|
||||
};
|
||||
|
||||
const clickShift = () => {
|
||||
shiftEnabled.value = !shiftEnabled.value;
|
||||
shiftAlpha.set(shiftEnabled.value ? 1 : 0);
|
||||
};
|
||||
|
||||
const clickAlt = () => {
|
||||
altEnabled.value = !altEnabled.value;
|
||||
altAlpha.set(altEnabled.value ? 1 : 0);
|
||||
};
|
||||
|
||||
const clickNum = (num: number) => {
|
||||
const bin = generateBinary([
|
||||
ctrlEnabled.value,
|
||||
shiftEnabled.value,
|
||||
altEnabled.value
|
||||
]);
|
||||
const code = (KeyCode.Digit0 + num) as KeyCode;
|
||||
gameKey.emitKey(code, bin, 'up', generateKeyboardEvent(code, bin));
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container loc={props.loc} cursor="pointer">
|
||||
<container loc={[0, 0, 180, 81]}>
|
||||
{nums
|
||||
.map((v, i) => {
|
||||
const col = i % 5;
|
||||
const row = Math.floor(i / 5);
|
||||
return [
|
||||
<g-rectr
|
||||
loc={ic2(col, row)}
|
||||
circle={[4]}
|
||||
stroke
|
||||
onClick={() => clickNum(v)}
|
||||
/>,
|
||||
<text
|
||||
text={v.toString()}
|
||||
loc={middle(col, row)}
|
||||
font={font}
|
||||
noevent
|
||||
/>
|
||||
];
|
||||
})
|
||||
.flat()}
|
||||
</container>
|
||||
<g-rectr
|
||||
loc={[41, 81, 36, 24]}
|
||||
circle={[4]}
|
||||
stroke
|
||||
fill
|
||||
fillStyle={ctrlColor.value}
|
||||
onClick={clickCtrl}
|
||||
></g-rectr>
|
||||
<text
|
||||
text="Ctrl"
|
||||
loc={[59, 93, void 0, void 0, 0.5, 0.5]}
|
||||
fillStyle={ctrlTextColor.value}
|
||||
font={font}
|
||||
noevent
|
||||
/>
|
||||
<g-rectr
|
||||
loc={[86, 81, 44, 24]}
|
||||
circle={[4]}
|
||||
stroke
|
||||
fill
|
||||
fillStyle={shiftColor.value}
|
||||
onClick={clickShift}
|
||||
></g-rectr>
|
||||
<text
|
||||
text="Shift"
|
||||
loc={[108, 93, void 0, void 0, 0.5, 0.5]}
|
||||
fillStyle={shiftTextColor.value}
|
||||
font={font}
|
||||
noevent
|
||||
/>
|
||||
<g-rectr
|
||||
loc={[139, 81, 30, 24]}
|
||||
circle={[4]}
|
||||
stroke
|
||||
fill
|
||||
fillStyle={altColor.value}
|
||||
onClick={clickAlt}
|
||||
></g-rectr>
|
||||
<text
|
||||
text="Alt"
|
||||
loc={[154, 93, void 0, void 0, 0.5, 0.5]}
|
||||
fillStyle={altTextColor.value}
|
||||
font={font}
|
||||
noevent
|
||||
/>
|
||||
<NumpadIcon loc={ic(0, 2)} strokeStyle="gold" onClick={numpad} />
|
||||
</container>
|
||||
);
|
||||
}, toolbarProps);
|
||||
|
||||
export const MixedToolbar = defineComponent<ReplayingProps>(props => {
|
||||
const inNumpad = ref(false);
|
||||
|
||||
const onNumpad = () => {
|
||||
inNumpad.value = !inNumpad.value;
|
||||
};
|
||||
|
||||
return () =>
|
||||
inNumpad.value ? (
|
||||
<NumpadToolbar
|
||||
loc={props.loc}
|
||||
onNumpad={onNumpad}
|
||||
state={props.state}
|
||||
/>
|
||||
) : props.status.replaying ? (
|
||||
<ReplayingToolbar
|
||||
loc={props.loc}
|
||||
status={props.status}
|
||||
state={props.state}
|
||||
/>
|
||||
) : (
|
||||
<PlayingToolbar
|
||||
loc={props.loc}
|
||||
onNumpad={onNumpad}
|
||||
state={props.state}
|
||||
/>
|
||||
);
|
||||
}, replayingProps);
|
||||
@@ -1,278 +0,0 @@
|
||||
import { GameUI, SetupComponentOptions } from '@motajs/system';
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import { Font } from '@motajs/render';
|
||||
import { Scroll, TextContent } from '@user/client-base';
|
||||
import { IHeroItemState, IHeroState } from '@user/data-base';
|
||||
import { IHeroAttr } from '@user/data-common';
|
||||
import { FULL_LOC, MAIN_HEIGHT, MAIN_WIDTH } from '../shared';
|
||||
import { IUIPropsBase } from './types';
|
||||
|
||||
export interface ToolBoxProps extends IUIPropsBase {
|
||||
/** 勇士状态对象 */
|
||||
heroState: IHeroState<IHeroAttr>;
|
||||
}
|
||||
|
||||
/** 道具栏组件的 props 声明 */
|
||||
const toolBoxProps = {
|
||||
props: ['controller', 'instance', 'state', 'heroState']
|
||||
} satisfies SetupComponentOptions<ToolBoxProps>;
|
||||
|
||||
/** 单列宽度,画面等分为永久道具、可消耗道具、道具信息三列 */
|
||||
const COLUMN_WIDTH = MAIN_WIDTH / 3;
|
||||
/** 列表标题占用的高度 */
|
||||
const HEADER_HEIGHT = 28;
|
||||
/** 列表区域起始纵坐标 */
|
||||
const LIST_TOP = HEADER_HEIGHT + 8;
|
||||
/** 列表区域高度,底部为退出按钮留出空间 */
|
||||
const LIST_HEIGHT = MAIN_HEIGHT - LIST_TOP - 36;
|
||||
/** 每行道具的高度 */
|
||||
const ROW_HEIGHT = 46;
|
||||
/** 行内图标的尺寸 */
|
||||
const ROW_ICON_SIZE = 32;
|
||||
/** 道具信息列起始横坐标 */
|
||||
const INFO_LEFT = COLUMN_WIDTH * 2;
|
||||
/** 道具说明区域起始纵坐标 */
|
||||
const DESC_TOP = 40;
|
||||
/** 道具说明区域高度,底部为退出按钮留出空间 */
|
||||
const DESC_HEIGHT = MAIN_HEIGHT - DESC_TOP - 36;
|
||||
|
||||
/** 列表标题字体 */
|
||||
const titleFont = Font.defaults({ size: 14, weight: 700 });
|
||||
/** 道具名称与数量字体 */
|
||||
const itemFont = Font.defaults({ size: 13 });
|
||||
/** 道具说明字体 */
|
||||
const descFont = Font.defaults({ size: 12 });
|
||||
|
||||
/** 道具栏组件 */
|
||||
export const ToolBox = defineComponent<ToolBoxProps>(props => {
|
||||
/** 勇士状态对象 */
|
||||
const hero = props.heroState;
|
||||
|
||||
/** 当前选中的道具图块数字,-1 表示未选中 */
|
||||
const selected = ref(-1);
|
||||
|
||||
/**
|
||||
* 枚举背包内该消耗类型的道具,过滤掉在道具栏隐藏的道具,
|
||||
* 并按数量从多到少排序,数量相同时按图块数字升序
|
||||
* @param consumable 保留的道具是否为可消耗道具
|
||||
*/
|
||||
const collect = (consumable: boolean) => {
|
||||
const list: IHeroItemState<IHeroAttr>[] = [];
|
||||
for (const pair of hero.items.iterateItems()) {
|
||||
const item = pair[1];
|
||||
if (item.raw.hideInToolbox === true) continue;
|
||||
if (item.consumable === consumable) list.push(item);
|
||||
}
|
||||
return list.sort((a, b) =>
|
||||
a.count !== b.count ? b.count - a.count : a.num - b.num
|
||||
);
|
||||
};
|
||||
|
||||
/** 永久道具列表 */
|
||||
const permItems = computed(() => collect(false));
|
||||
/** 可消耗道具列表 */
|
||||
const consItems = computed(() => collect(true));
|
||||
|
||||
/** 当前选中的道具状态,未选中时为 null */
|
||||
const selectedItem = computed<IHeroItemState<IHeroAttr> | null>(() => {
|
||||
if (selected.value < 0) return null;
|
||||
for (const item of permItems.value) {
|
||||
if (item.num === selected.value) return item;
|
||||
}
|
||||
for (const item of consItems.value) {
|
||||
if (item.num === selected.value) return item;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
/**
|
||||
* 点击道具行:未选中时仅选中,再次点击已选中的道具时使用该道具
|
||||
* @param item 被点击的道具状态
|
||||
*/
|
||||
const clickItem = (item: IHeroItemState<IHeroAttr>) => {
|
||||
if (selected.value === item.num) {
|
||||
hero.items.useItem(item.num);
|
||||
return;
|
||||
}
|
||||
selected.value = item.num;
|
||||
};
|
||||
|
||||
/** 关闭当前道具栏界面 */
|
||||
const exit = () => props.controller.close(props.instance);
|
||||
|
||||
return () => {
|
||||
const current = selectedItem.value;
|
||||
return (
|
||||
<container loc={FULL_LOC}>
|
||||
{/* 永久道具列 */}
|
||||
<text
|
||||
text="永久道具"
|
||||
loc={[8, HEADER_HEIGHT / 2]}
|
||||
anc={[0, 0.5]}
|
||||
font={titleFont}
|
||||
fillStyle="gold"
|
||||
/>
|
||||
{/* 可消耗道具列 */}
|
||||
<text
|
||||
text="可消耗道具"
|
||||
loc={[COLUMN_WIDTH + 8, HEADER_HEIGHT / 2]}
|
||||
anc={[0, 0.5]}
|
||||
font={titleFont}
|
||||
fillStyle="gold"
|
||||
/>
|
||||
<Scroll noscroll loc={[0, LIST_TOP, COLUMN_WIDTH, LIST_HEIGHT]}>
|
||||
{permItems.value.map((item, i) => {
|
||||
const isSelected = item.num === selected.value;
|
||||
return (
|
||||
<container
|
||||
key={item.num}
|
||||
loc={[
|
||||
0,
|
||||
i * ROW_HEIGHT,
|
||||
COLUMN_WIDTH,
|
||||
ROW_HEIGHT
|
||||
]}
|
||||
cursor="pointer"
|
||||
onClick={() => clickItem(item)}
|
||||
>
|
||||
<g-rectr
|
||||
loc={[
|
||||
4,
|
||||
2,
|
||||
COLUMN_WIDTH - 8,
|
||||
ROW_HEIGHT - 4
|
||||
]}
|
||||
circle={[4]}
|
||||
fill
|
||||
fillStyle={
|
||||
isSelected
|
||||
? 'rgba(255, 204, 0, 0.28)'
|
||||
: 'rgba(255, 255, 255, 0.05)'
|
||||
}
|
||||
/>
|
||||
<icon
|
||||
icon={item.num}
|
||||
loc={[10, 7, ROW_ICON_SIZE, ROW_ICON_SIZE]}
|
||||
/>
|
||||
<text
|
||||
text={item.raw.name}
|
||||
loc={[50, ROW_HEIGHT / 2]}
|
||||
anc={[0, 0.5]}
|
||||
font={itemFont}
|
||||
fillStyle="#eeeeee"
|
||||
/>
|
||||
<text
|
||||
text={core.formatBigNumber(item.count)}
|
||||
loc={[COLUMN_WIDTH - 8, ROW_HEIGHT / 2]}
|
||||
anc={[1, 0.5]}
|
||||
font={itemFont}
|
||||
fillStyle="#cccccc"
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
})}
|
||||
</Scroll>
|
||||
<Scroll
|
||||
noscroll
|
||||
loc={[COLUMN_WIDTH, LIST_TOP, COLUMN_WIDTH, LIST_HEIGHT]}
|
||||
>
|
||||
{consItems.value.map((item, i) => {
|
||||
const isSelected = item.num === selected.value;
|
||||
return (
|
||||
<container
|
||||
key={item.num}
|
||||
loc={[
|
||||
0,
|
||||
i * ROW_HEIGHT,
|
||||
COLUMN_WIDTH,
|
||||
ROW_HEIGHT
|
||||
]}
|
||||
cursor="pointer"
|
||||
onClick={() => clickItem(item)}
|
||||
>
|
||||
<g-rectr
|
||||
loc={[
|
||||
4,
|
||||
2,
|
||||
COLUMN_WIDTH - 8,
|
||||
ROW_HEIGHT - 4
|
||||
]}
|
||||
circle={[4]}
|
||||
fill
|
||||
fillStyle={
|
||||
isSelected
|
||||
? 'rgba(255, 204, 0, 0.28)'
|
||||
: 'rgba(255, 255, 255, 0.05)'
|
||||
}
|
||||
/>
|
||||
<icon
|
||||
icon={item.num}
|
||||
loc={[10, 7, ROW_ICON_SIZE, ROW_ICON_SIZE]}
|
||||
/>
|
||||
<text
|
||||
text={item.raw.name}
|
||||
loc={[50, ROW_HEIGHT / 2]}
|
||||
anc={[0, 0.5]}
|
||||
font={itemFont}
|
||||
fillStyle="#eeeeee"
|
||||
/>
|
||||
<text
|
||||
text={core.formatBigNumber(item.count)}
|
||||
loc={[COLUMN_WIDTH - 8, ROW_HEIGHT / 2]}
|
||||
anc={[1, 0.5]}
|
||||
font={itemFont}
|
||||
fillStyle="#cccccc"
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
})}
|
||||
</Scroll>
|
||||
{/* 道具名称 */}
|
||||
<text
|
||||
text={current ? current.raw.name : '未选择道具'}
|
||||
loc={[INFO_LEFT + 8, 14]}
|
||||
anc={[0, 0]}
|
||||
font={titleFont}
|
||||
fillStyle="gold"
|
||||
/>
|
||||
{/* 道具说明 */}
|
||||
<Scroll
|
||||
noscroll
|
||||
loc={[INFO_LEFT, DESC_TOP, COLUMN_WIDTH, DESC_HEIGHT]}
|
||||
>
|
||||
{!current && (
|
||||
<text
|
||||
text="未选择道具"
|
||||
loc={[8, 4]}
|
||||
anc={[0, 0]}
|
||||
font={descFont}
|
||||
fillStyle="#888888"
|
||||
/>
|
||||
)}
|
||||
{current && (
|
||||
<TextContent
|
||||
text={current.raw.text}
|
||||
width={COLUMN_WIDTH - 16}
|
||||
loc={[8, 4, COLUMN_WIDTH - 16, DESC_HEIGHT - 8]}
|
||||
font={descFont}
|
||||
fillStyle="#dddddd"
|
||||
/>
|
||||
)}
|
||||
</Scroll>
|
||||
{/* 右下角退出按钮 */}
|
||||
<text
|
||||
text="退出"
|
||||
loc={[MAIN_WIDTH - 12, MAIN_HEIGHT - 10]}
|
||||
anc={[1, 1]}
|
||||
font={titleFont}
|
||||
cursor="pointer"
|
||||
fillStyle="#ffffff"
|
||||
onClick={exit}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
};
|
||||
}, toolBoxProps);
|
||||
|
||||
/** 道具栏的 UI 控制器 */
|
||||
export const ToolBoxUI = new GameUI('toolbox', ToolBox);
|
||||
@@ -1,568 +0,0 @@
|
||||
import {
|
||||
ElementLocator,
|
||||
IActionEvent,
|
||||
IActionEventBase,
|
||||
IWheelEvent,
|
||||
MotaOffscreenCanvas2D,
|
||||
Font
|
||||
} from '@motajs/render';
|
||||
import { BaseProps } from '@motajs/render-vue';
|
||||
import {
|
||||
GameUI,
|
||||
IUIController,
|
||||
SetupComponentOptions,
|
||||
IUIDefaultPropsBase
|
||||
} from '@motajs/system';
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
markRaw,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch
|
||||
} from 'vue';
|
||||
import { FloorSelector } from '@user/client-base';
|
||||
import { clamp, mean } from 'lodash-es';
|
||||
import { StatisticsDataOneFloor } from './statistics';
|
||||
import { Tip, TipExpose, IGameUIPropsBase } from '@user/client-base';
|
||||
import { useKey } from '../render/use';
|
||||
import {
|
||||
ENABLE_RIGHT_STATUS_BAR,
|
||||
FULL_LOC,
|
||||
HALF_MAP_WIDTH,
|
||||
HALF_STATUS_WIDTH,
|
||||
MAIN_HEIGHT,
|
||||
MAP_HEIGHT,
|
||||
MAP_WIDTH,
|
||||
RIGHT_STATUS_POS,
|
||||
STATUS_BAR_HEIGHT,
|
||||
STATUS_BAR_WIDTH
|
||||
} from '../shared';
|
||||
|
||||
export interface ViewMapProps extends IUIDefaultPropsBase, BaseProps {
|
||||
loc: ElementLocator;
|
||||
floorId?: FloorIds;
|
||||
}
|
||||
|
||||
const viewMapProps = {
|
||||
props: ['loc', 'floorId', 'controller', 'instance']
|
||||
} satisfies SetupComponentOptions<ViewMapProps>;
|
||||
|
||||
export const ViewMap = defineComponent<ViewMapProps>(props => {
|
||||
const nowFloorId = core.status.floorId;
|
||||
|
||||
// const layerGroupExtends: ILayerGroupRenderExtends[] = [
|
||||
// new FloorDamageExtends(),
|
||||
// new FloorItemDetail(),
|
||||
// new LayerGroupAnimate()
|
||||
// ];
|
||||
|
||||
const restHeight = STATUS_BAR_HEIGHT - 292;
|
||||
const col = restHeight / 4;
|
||||
const loc1: ElementLocator = [HALF_STATUS_WIDTH, col * 1 + 292];
|
||||
const loc2: ElementLocator = [HALF_STATUS_WIDTH, col * 2 + 292];
|
||||
const loc3: ElementLocator = [HALF_STATUS_WIDTH, col * 3 + 292];
|
||||
|
||||
const rightFont = new Font(Font.defaultFamily, 15);
|
||||
|
||||
const viewableFloor = markRaw(
|
||||
core.floorIds.filter(v => {
|
||||
return (
|
||||
!core.floors[v].cannotViewMap &&
|
||||
!core.status?.hero?.flags?.__removed__?.includes(v)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// const group = ref<LayerGroup>();
|
||||
const tip = ref<TipExpose>();
|
||||
const statistics = shallowRef<StatisticsDataOneFloor>();
|
||||
|
||||
const now = ref(0);
|
||||
if (props.floorId) {
|
||||
const index = viewableFloor.indexOf(props.floorId);
|
||||
if (index !== -1) now.value = index;
|
||||
}
|
||||
|
||||
const floorId = computed(() => viewableFloor[now.value]);
|
||||
|
||||
//#region 按键实现
|
||||
|
||||
const [key] = useKey();
|
||||
key.realize('@viewMap_up', () => changeFloor(1), { type: 'down-repeat' })
|
||||
.realize('@viewMap_down', () => changeFloor(-1), {
|
||||
type: 'down-repeat'
|
||||
})
|
||||
.realize('@viewMap_up_ten', () => changeFloor(10))
|
||||
.realize('@viewMap_down_ten', () => changeFloor(-10))
|
||||
.realize('@viewMap_book', () => openBook())
|
||||
.realize('@viewMap_fly', () => fly())
|
||||
// .realize('@viewMap_reset', () => resetCamera())
|
||||
.realize('confirm', () => close())
|
||||
.realize('exit', (_, code, assist) => {
|
||||
// 如果按键不能触发怪物手册,则关闭界面,因为怪物手册和退出默认使用同一个按键,需要特判
|
||||
if (!key.willEmit(code, assist, 'book')) {
|
||||
props.controller.close(props.instance);
|
||||
}
|
||||
});
|
||||
|
||||
//#region 功能函数
|
||||
|
||||
const close = () => {
|
||||
props.controller.close(props.instance);
|
||||
};
|
||||
|
||||
const format = (num?: number) => {
|
||||
return core.formatBigNumber(num ?? 0, 6);
|
||||
};
|
||||
|
||||
const changeTo = (index: number) => {
|
||||
const res = clamp(index, 0, viewableFloor.length - 1);
|
||||
now.value = res;
|
||||
};
|
||||
|
||||
const changeFloor = (delta: number) => {
|
||||
changeTo(now.value + delta);
|
||||
};
|
||||
|
||||
const openBook = () => core.openBook(true);
|
||||
|
||||
const fly = () => {
|
||||
if (!core.hasItem('fly')) return;
|
||||
const id = viewableFloor[now.value];
|
||||
const success = core.flyTo(id);
|
||||
if (success) close();
|
||||
else tip.value?.drawTip(`无法飞往${core.floors[id].title}`);
|
||||
};
|
||||
|
||||
// const resetCamera = () => {
|
||||
// group.value?.camera.reset();
|
||||
// group.value?.update();
|
||||
// };
|
||||
|
||||
//#region 渐变渲染
|
||||
|
||||
const topAlpha = ref(0.7);
|
||||
const bottomAlpha = ref(0.7);
|
||||
|
||||
let topGradient: CanvasGradient | null = null;
|
||||
let bottomGradient: CanvasGradient | null = null;
|
||||
|
||||
const getTopGradient = (ctx: CanvasRenderingContext2D) => {
|
||||
if (topGradient) return topGradient;
|
||||
topGradient = ctx.createLinearGradient(0, 0, 0, 64);
|
||||
topGradient.addColorStop(0, 'rgba(0,0,0,1)');
|
||||
topGradient.addColorStop(0.75, 'rgba(0,0,0,0.5)');
|
||||
topGradient.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
return topGradient;
|
||||
};
|
||||
|
||||
const getBottomGradient = (ctx: CanvasRenderingContext2D) => {
|
||||
if (bottomGradient) return bottomGradient;
|
||||
bottomGradient = ctx.createLinearGradient(0, 64, 0, 0);
|
||||
bottomGradient.addColorStop(0, 'rgba(0,0,0,1)');
|
||||
bottomGradient.addColorStop(0.75, 'rgba(0,0,0,0.5)');
|
||||
bottomGradient.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
return bottomGradient;
|
||||
};
|
||||
|
||||
const renderTop = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const ctx = canvas.ctx;
|
||||
ctx.fillStyle = getTopGradient(ctx);
|
||||
ctx.fillRect(0, 0, MAP_WIDTH, 64);
|
||||
};
|
||||
|
||||
const renderBottom = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const ctx = canvas.ctx;
|
||||
ctx.fillStyle = getBottomGradient(ctx);
|
||||
ctx.fillRect(0, 0, MAP_HEIGHT, 64);
|
||||
};
|
||||
|
||||
const enterTop = () => (topAlpha.value = 0.9);
|
||||
const enterBottom = () => (bottomAlpha.value = 0.9);
|
||||
const leaveTop = () => (topAlpha.value = 0.7);
|
||||
const leaveBottom = () => (bottomAlpha.value = 0.7);
|
||||
|
||||
//#region 地图渲染
|
||||
|
||||
// const renderLayer = (floorId: FloorIds) => {
|
||||
// const binder = group.value?.getExtends(
|
||||
// 'floor-binder'
|
||||
// ) as LayerGroupFloorBinder;
|
||||
// binder.bindFloor(floorId);
|
||||
// group.value?.camera.reset();
|
||||
// core.status.floorId = floorId;
|
||||
// core.status.thisMap = core.status.maps[floorId];
|
||||
// statistics.value = calculateStatisticsOne(floorId);
|
||||
// };
|
||||
|
||||
// const moveCamera = (dx: number, dy: number) => {
|
||||
// const camera = group.value?.camera;
|
||||
// if (!camera) return;
|
||||
// camera.translate(dx / camera.scaleX, dy / camera.scaleX);
|
||||
// group.value?.update();
|
||||
// };
|
||||
|
||||
// const scaleCamera = (scale: number, x: number, y: number) => {
|
||||
// const camera = group.value?.camera;
|
||||
// if (!camera) return;
|
||||
// const [cx, cy] = camera.untransformed(x, y);
|
||||
// camera.translate(cx, cy);
|
||||
// camera.scale(scale);
|
||||
// camera.translate(-cx, -cy);
|
||||
// group.value?.update();
|
||||
// };
|
||||
|
||||
//#region 事件监听
|
||||
|
||||
const clickTop = (ev: IActionEvent) => {
|
||||
const col = MAP_WIDTH / 3;
|
||||
if (ev.offsetX < col * 2) {
|
||||
changeFloor(1);
|
||||
} else {
|
||||
// resetCamera();
|
||||
}
|
||||
};
|
||||
|
||||
const clickBottom = (ev: IActionEvent) => {
|
||||
const col = MAP_WIDTH / 3;
|
||||
if (ev.offsetX < col) {
|
||||
openBook();
|
||||
} else if (ev.offsetX < col * 2) {
|
||||
changeFloor(-1);
|
||||
} else {
|
||||
fly();
|
||||
}
|
||||
};
|
||||
|
||||
//#region 地图交互
|
||||
|
||||
let mouseDown = false;
|
||||
let moved = false;
|
||||
let scaled = false;
|
||||
let lastMoveX = 0;
|
||||
let lastMoveY = 0;
|
||||
let lastDis = 0;
|
||||
let movement = 0;
|
||||
|
||||
const touches = new Map<number, IActionEvent>();
|
||||
|
||||
const downMap = (ev: IActionEvent) => {
|
||||
moved = false;
|
||||
lastMoveX = ev.offsetX;
|
||||
lastMoveY = ev.offsetY;
|
||||
movement = 0;
|
||||
|
||||
if (ev.touch) {
|
||||
touches.set(ev.identifier, ev);
|
||||
if (touches.size >= 2) {
|
||||
const [touch1, touch2] = touches.values();
|
||||
lastDis = Math.hypot(
|
||||
touch1.offsetX - touch2.offsetX,
|
||||
touch1.offsetY - touch2.offsetY
|
||||
);
|
||||
}
|
||||
} else {
|
||||
mouseDown = true;
|
||||
}
|
||||
};
|
||||
|
||||
const upMap = (ev: IActionEvent) => {
|
||||
if (ev.touch) {
|
||||
touches.delete(ev.identifier);
|
||||
} else {
|
||||
mouseDown = false;
|
||||
}
|
||||
if (touches.size === 0) {
|
||||
scaled = false;
|
||||
}
|
||||
};
|
||||
|
||||
const move = (ev: IActionEvent) => {
|
||||
if (moved) {
|
||||
const dx = ev.offsetX - lastMoveX;
|
||||
const dy = ev.offsetY - lastMoveY;
|
||||
movement += Math.hypot(dx, dy);
|
||||
// moveCamera(dx, dy);
|
||||
}
|
||||
moved = true;
|
||||
lastMoveX = ev.offsetX;
|
||||
lastMoveY = ev.offsetY;
|
||||
};
|
||||
|
||||
const moveMap = (ev: IActionEvent) => {
|
||||
if (ev.touch) {
|
||||
if (touches.size === 0) return;
|
||||
else if (touches.size === 1) {
|
||||
// 移动
|
||||
if (scaled) return;
|
||||
move(ev);
|
||||
} else {
|
||||
// 缩放
|
||||
const [touch1, touch2] = touches.values();
|
||||
const cx = mean([touch1.offsetX, touch2.offsetX]);
|
||||
const cy = mean([touch1.offsetY, touch2.offsetY]);
|
||||
const dis = Math.hypot(
|
||||
touch1.offsetX - touch2.offsetX,
|
||||
touch1.offsetY - touch2.offsetY
|
||||
);
|
||||
const scale = dis / lastDis;
|
||||
if (!scaled) {
|
||||
lastDis = dis;
|
||||
return;
|
||||
}
|
||||
if (!isFinite(scale) || scale === 0) return;
|
||||
// scaleCamera(scale, cx, cy);
|
||||
}
|
||||
} else {
|
||||
if (mouseDown) {
|
||||
move(ev);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const leaveMap = (ev: IActionEventBase) => {
|
||||
if (ev.touch) {
|
||||
touches.delete(ev.identifier);
|
||||
} else {
|
||||
mouseDown = false;
|
||||
}
|
||||
};
|
||||
|
||||
const wheelMap = (ev: IWheelEvent) => {
|
||||
if (ev.altKey) {
|
||||
// const scale = ev.wheelY < 0 ? 1.1 : 0.9;
|
||||
// scaleCamera(scale, ev.offsetX, ev.offsetY);
|
||||
} else if (ev.ctrlKey) {
|
||||
changeFloor(-Math.sign(ev.wheelY) * 10);
|
||||
} else {
|
||||
changeFloor(-Math.sign(ev.wheelY));
|
||||
}
|
||||
};
|
||||
|
||||
const clickMap = (ev: IActionEvent) => {
|
||||
if (movement > 5) return;
|
||||
if (ev.touch) {
|
||||
if (touches.size === 0) {
|
||||
close();
|
||||
}
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// renderLayer(floorId.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
core.status.floorId = nowFloorId;
|
||||
core.status.thisMap = core.status.maps[nowFloorId];
|
||||
});
|
||||
|
||||
watch(floorId, value => {
|
||||
// renderLayer(value);
|
||||
});
|
||||
|
||||
//#region 组件树
|
||||
|
||||
return () => (
|
||||
<container loc={props.loc} nocache>
|
||||
<g-rect fillStyle="black" fill loc={FULL_LOC} />
|
||||
<g-rect stroke zIndex={100} loc={FULL_LOC} noevent />
|
||||
<g-line
|
||||
line={[STATUS_BAR_WIDTH, 0, STATUS_BAR_WIDTH, MAIN_HEIGHT]}
|
||||
lineWidth={1}
|
||||
/>
|
||||
<g-line
|
||||
line={[RIGHT_STATUS_POS, 0, RIGHT_STATUS_POS, MAIN_HEIGHT]}
|
||||
lineWidth={1}
|
||||
/>
|
||||
<FloorSelector
|
||||
loc={[0, 0, STATUS_BAR_WIDTH, MAIN_HEIGHT]}
|
||||
floors={viewableFloor}
|
||||
v-model:now={now.value}
|
||||
onClose={close}
|
||||
/>
|
||||
{/* <layer-group
|
||||
ref={group}
|
||||
ex={layerGroupExtends}
|
||||
loc={[STATUS_BAR_WIDTH, 0, MAP_WIDTH, MAP_HEIGHT]}
|
||||
onDown={downMap}
|
||||
onMove={moveMap}
|
||||
onUp={upMap}
|
||||
onLeave={leaveMap}
|
||||
onWheel={wheelMap}
|
||||
onClick={clickMap}
|
||||
>
|
||||
<layer layer="bg" zIndex={10}></layer>
|
||||
<layer layer="bg2" zIndex={20}></layer>
|
||||
<layer layer="event" zIndex={30}></layer>
|
||||
<layer layer="fg" zIndex={40}></layer>
|
||||
<layer layer="fg2" zIndex={50}></layer>
|
||||
</layer-group> */}
|
||||
<Tip
|
||||
ref={tip}
|
||||
zIndex={40}
|
||||
loc={[STATUS_BAR_WIDTH + 8, 8, 200, 32]}
|
||||
pad={[12, 6]}
|
||||
corner={16}
|
||||
/>
|
||||
<custom
|
||||
loc={[STATUS_BAR_WIDTH, 0, MAP_WIDTH, 64]}
|
||||
render={renderTop}
|
||||
alpha={topAlpha.value}
|
||||
zIndex={10}
|
||||
cursor="pointer"
|
||||
onEnter={enterTop}
|
||||
onLeave={leaveTop}
|
||||
onClick={clickTop}
|
||||
/>
|
||||
<custom
|
||||
loc={[STATUS_BAR_WIDTH, MAP_HEIGHT - 64, MAP_WIDTH, 64]}
|
||||
render={renderBottom}
|
||||
alpha={bottomAlpha.value}
|
||||
zIndex={10}
|
||||
cursor="pointer"
|
||||
onEnter={enterBottom}
|
||||
onLeave={leaveBottom}
|
||||
onClick={clickBottom}
|
||||
/>
|
||||
<text
|
||||
text="上移地图"
|
||||
loc={[HALF_MAP_WIDTH + STATUS_BAR_WIDTH, 24]}
|
||||
anc={[0.5, 0.5]}
|
||||
zIndex={20}
|
||||
noevent
|
||||
/>
|
||||
<text
|
||||
text="下移地图"
|
||||
loc={[HALF_MAP_WIDTH + STATUS_BAR_WIDTH, MAP_HEIGHT - 24]}
|
||||
anc={[0.5, 0.5]}
|
||||
zIndex={20}
|
||||
noevent
|
||||
/>
|
||||
<text
|
||||
text="「 怪物手册 」"
|
||||
loc={[32 + STATUS_BAR_WIDTH, MAP_HEIGHT - 24]}
|
||||
anc={[0, 0.5]}
|
||||
zIndex={20}
|
||||
noevent
|
||||
/>
|
||||
<text
|
||||
text="「 传送至此 」"
|
||||
loc={[RIGHT_STATUS_POS - 32, MAP_HEIGHT - 24]}
|
||||
anc={[1, 0.5]}
|
||||
zIndex={20}
|
||||
noevent
|
||||
/>
|
||||
<text
|
||||
text="「 重置视角 」"
|
||||
loc={[RIGHT_STATUS_POS - 32, 24]}
|
||||
anc={[1, 0.5]}
|
||||
zIndex={20}
|
||||
noevent
|
||||
/>
|
||||
<container
|
||||
loc={[RIGHT_STATUS_POS, 0, STATUS_BAR_WIDTH, STATUS_BAR_HEIGHT]}
|
||||
hidden={!ENABLE_RIGHT_STATUS_BAR}
|
||||
>
|
||||
<text
|
||||
text="鼠标 / 单指拖动地图"
|
||||
font={rightFont}
|
||||
loc={[90, 24]}
|
||||
anc={[0.5, 0.5]}
|
||||
fillStyle="yellow"
|
||||
/>
|
||||
<text
|
||||
text="Alt+滚轮 / 双指缩放地图"
|
||||
font={rightFont}
|
||||
loc={[90, 48]}
|
||||
anc={[0.5, 0.5]}
|
||||
fillStyle="yellow"
|
||||
/>
|
||||
<text
|
||||
text="Ctrl+滚轮 / 滚轮切换地图"
|
||||
font={rightFont}
|
||||
loc={[90, 72]}
|
||||
anc={[0.5, 0.5]}
|
||||
fillStyle="yellow"
|
||||
/>
|
||||
<g-line line={[12, 96, 168, 96]} lineWidth={1} />
|
||||
<text
|
||||
text={`怪物数量:${statistics.value?.enemyCount}`}
|
||||
loc={[20, 120]}
|
||||
anc={[0, 0.5]}
|
||||
/>
|
||||
<text
|
||||
text={`血瓶数量:${statistics.value?.potionCount}`}
|
||||
loc={[20, 144]}
|
||||
anc={[0, 0.5]}
|
||||
/>
|
||||
<text
|
||||
text={`宝石数量:${statistics.value?.gemCount}`}
|
||||
loc={[20, 168]}
|
||||
anc={[0, 0.5]}
|
||||
/>
|
||||
<text
|
||||
text={`血瓶数值:${format(statistics.value?.potionValue)}`}
|
||||
loc={[20, 192]}
|
||||
anc={[0, 0.5]}
|
||||
/>
|
||||
<text
|
||||
text={`攻击数值:${format(statistics.value?.atkValue)}`}
|
||||
loc={[20, 216]}
|
||||
anc={[0, 0.5]}
|
||||
/>
|
||||
<text
|
||||
text={`防御数值:${format(statistics.value?.defValue)}`}
|
||||
loc={[20, 240]}
|
||||
anc={[0, 0.5]}
|
||||
/>
|
||||
<text
|
||||
text={`智慧数值:${format(statistics.value?.mdefValue)}`}
|
||||
loc={[20, 264]}
|
||||
anc={[0, 0.5]}
|
||||
/>
|
||||
<g-line line={[12, 292, 168, 292]} lineWidth={1} />
|
||||
<text
|
||||
text="「 怪物手册 」"
|
||||
loc={loc1}
|
||||
anc={[0.5, 0.5]}
|
||||
cursor="pointer"
|
||||
onClick={openBook}
|
||||
/>
|
||||
<text
|
||||
text="「 传送至此 」"
|
||||
loc={loc2}
|
||||
anc={[0.5, 0.5]}
|
||||
cursor="pointer"
|
||||
onClick={fly}
|
||||
/>
|
||||
<text
|
||||
text="「 重置视角 」"
|
||||
loc={loc3}
|
||||
anc={[0.5, 0.5]}
|
||||
cursor="pointer"
|
||||
// onClick={resetCamera}
|
||||
/>
|
||||
</container>
|
||||
</container>
|
||||
);
|
||||
}, viewMapProps);
|
||||
|
||||
export const ViewMapUI = new GameUI('view-map', ViewMap);
|
||||
|
||||
export function openViewMap(
|
||||
controller: IUIController<IGameUIPropsBase>,
|
||||
loc: ElementLocator,
|
||||
props?: ViewMapProps
|
||||
) {
|
||||
controller.open(ViewMapUI, {
|
||||
...props,
|
||||
loc,
|
||||
floorId: core.status.floorId
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user