mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-09-06 16:31:46 +08:00
504 lines
15 KiB
TypeScript
504 lines
15 KiB
TypeScript
import {
|
|
ElementLocator,
|
|
IActionEvent,
|
|
IActionEventBase,
|
|
IWheelEvent,
|
|
MotaOffscreenCanvas2D
|
|
} from '@motajs/render-core';
|
|
import { BaseProps } from '@motajs/render-vue';
|
|
import {
|
|
GameUI,
|
|
IUIMountable,
|
|
SetupComponentOptions,
|
|
UIComponentProps
|
|
} from '@motajs/system-ui';
|
|
import {
|
|
computed,
|
|
defineComponent,
|
|
markRaw,
|
|
onMounted,
|
|
onUnmounted,
|
|
ref,
|
|
shallowRef,
|
|
watch
|
|
} from 'vue';
|
|
import { FloorSelector } from '../components/floorSelect';
|
|
import {
|
|
ILayerGroupRenderExtends,
|
|
FloorDamageExtends,
|
|
FloorItemDetail,
|
|
LayerGroupAnimate,
|
|
LayerGroup,
|
|
LayerGroupFloorBinder
|
|
} from '../elements';
|
|
import { LayerGroupHalo } from '../legacy/halo';
|
|
import { LayerGroupPortal } from '../legacy/portal';
|
|
import { Font } from '@motajs/render-style';
|
|
import { clamp, mean } from 'lodash-es';
|
|
import { calculateStatisticsOne, StatisticsDataOneFloor } from './statistics';
|
|
import { Tip, TipExpose } from '../components';
|
|
import { useKey } from '../use';
|
|
|
|
export interface ViewMapProps extends UIComponentProps, 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 LayerGroupPortal(),
|
|
new LayerGroupHalo(),
|
|
new LayerGroupAnimate()
|
|
];
|
|
|
|
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());
|
|
|
|
//#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 = () => {
|
|
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, 480, 64);
|
|
};
|
|
|
|
const renderBottom = (canvas: MotaOffscreenCanvas2D) => {
|
|
const ctx = canvas.ctx;
|
|
ctx.fillStyle = getBottomGradient(ctx);
|
|
ctx.fillRect(0, 0, 480, 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 地图交互
|
|
|
|
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={[0, 0, 840, 480]} />
|
|
<g-rect stroke zIndex={100} loc={[0, 0, 840, 480]} noevent />
|
|
<g-line line={[180, 0, 180, 480]} lineWidth={1} />
|
|
<g-line line={[180 + 480, 0, 180 + 480, 480]} lineWidth={1} />
|
|
<FloorSelector
|
|
loc={[0, 0, 180, 480]}
|
|
floors={viewableFloor}
|
|
v-model:now={now.value}
|
|
onClose={close}
|
|
/>
|
|
<layer-group
|
|
ref={group}
|
|
ex={layerGroupExtends}
|
|
loc={[180, 0, 480, 480]}
|
|
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={[188, 8, 200, 32]}
|
|
pad={[12, 6]}
|
|
corner={16}
|
|
/>
|
|
<sprite
|
|
loc={[180, 0, 480, 64]}
|
|
render={renderTop}
|
|
alpha={topAlpha.value}
|
|
zIndex={10}
|
|
cursor="pointer"
|
|
onEnter={enterTop}
|
|
onLeave={leaveTop}
|
|
onClick={() => changeFloor(1)}
|
|
/>
|
|
<sprite
|
|
loc={[180, 416, 480, 64]}
|
|
render={renderBottom}
|
|
alpha={bottomAlpha.value}
|
|
zIndex={10}
|
|
cursor="pointer"
|
|
onEnter={enterBottom}
|
|
onLeave={leaveBottom}
|
|
onClick={() => changeFloor(-1)}
|
|
/>
|
|
<text
|
|
text="上移地图"
|
|
loc={[420, 24]}
|
|
anc={[0.5, 0.5]}
|
|
zIndex={20}
|
|
noevent
|
|
/>
|
|
<text
|
|
text="下移地图"
|
|
loc={[420, 456]}
|
|
anc={[0.5, 0.5]}
|
|
zIndex={20}
|
|
noevent
|
|
/>
|
|
<container loc={[660, 0, 180, 480]}>
|
|
<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={[90, 330]}
|
|
anc={[0.5, 0.5]}
|
|
cursor="pointer"
|
|
onClick={openBook}
|
|
/>
|
|
<text
|
|
text="「 传送至此 」"
|
|
loc={[90, 380]}
|
|
anc={[0.5, 0.5]}
|
|
cursor="pointer"
|
|
onClick={fly}
|
|
/>
|
|
<text
|
|
text="「 重置视角 」"
|
|
loc={[90, 430]}
|
|
anc={[0.5, 0.5]}
|
|
cursor="pointer"
|
|
onClick={resetCamera}
|
|
/>
|
|
</container>
|
|
</container>
|
|
);
|
|
}, viewMapProps);
|
|
|
|
export const ViewMapUI = new GameUI('view-map', ViewMap);
|
|
|
|
export function openViewMap(
|
|
controller: IUIMountable,
|
|
loc: ElementLocator,
|
|
props?: ViewMapProps
|
|
) {
|
|
controller.open(ViewMapUI, {
|
|
...props,
|
|
loc,
|
|
floorId: core.status.floorId
|
|
});
|
|
}
|