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; export const ViewMap = defineComponent(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(); const tip = ref(); const statistics = shallowRef(); 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(); 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 () => ( changeFloor(1)} /> changeFloor(-1)} /> ); }, 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 }); }