feat: 浏览地图

This commit is contained in:
AncTe committed 2025-08-19 15:06:34 +08:00
1 parent 398de70fee
commit 574d765c69
15 files changed
+615 -35

No files matched your search

@@ -434,36 +434,85 @@ gameKey
defaults: KeyCode.PageUp
})
// #region 存档界面
.group('@ui_save', 'save')
.group('@ui_save', '存档界面')
.register({
id: '@save_pageUp',
name: '存档向后翻页',
name: '向后翻页',
defaults: KeyCode.PageUp
})
.register({
id: '@save_pageDown',
name: '存档向前翻页',
name: '向前翻页',
defaults: KeyCode.PageDown
})
.register({
id: '@save_up',
name: '存档选择框向上',
name: '选择框向上',
defaults: KeyCode.UpArrow
})
.register({
id: '@save_down',
name: '存档选择框向下',
name: '选择框向下',
defaults: KeyCode.DownArrow
})
.register({
id: '@save_left',
name: '存档选择框向左',
name: '选择框向左',
defaults: KeyCode.LeftArrow
})
.register({
id: '@save_right',
name: '存档选择框向右',
name: '选择框向右',
defaults: KeyCode.RightArrow
})
//#region 浏览地图
.group('@ui_viewMap', '浏览地图')
.register({
id: '@viewMap_up_1',
name: '下一层地图_1',
defaults: KeyCode.UpArrow
})
.register({
id: '@viewMap_up_2',
name: '下一层地图_2',
defaults: KeyCode.PageUp
})
.register({
id: '@viewMap_down_1',
name: '上一层地图_1',
defaults: KeyCode.DownArrow
})
.register({
id: '@viewMap_down_2',
name: '上一层地图_2',
defaults: KeyCode.PageDown
})
.register({
id: '@viewMap_up_ten',
name: '下十层地图',
defaults: KeyCode.UpArrow,
ctrl: true
})
.register({
id: '@viewMap_down_ten',
name: '上十层地图',
defaults: KeyCode.DownArrow,
ctrl: true
})
.register({
id: '@viewMap_book',
name: '怪物手册',
defaults: KeyCode.KeyX
})
.register({
id: '@viewMap_fly',
name: '传送至',
defaults: KeyCode.KeyG
})
.register({
id: '@viewMap_reset',
name: '重置视角',
defaults: KeyCode.KeyR
});
// #endregion
@@ -492,9 +541,6 @@ gameKey
.realize('shop', () => {
core.openQuickShop(true);
})
.realize('viewMap', () => {
core.ui._drawViewMaps();
})
.realize('skillTree', () => {
core.useItem('skill1', true);
})
@@ -6,7 +6,8 @@ import {
openStatistics,
saveLoad,
openSettings,
ReplaySettingsUI
ReplaySettingsUI,
openViewMap
} from './ui';
export function createAction() {
@@ -27,5 +28,8 @@ export function createAction() {
mainUIController.open(ReplaySettingsUI, {
loc: [420, 240, void 0, void 0, 0.5, 0.5]
});
})
.realize('viewMap', () => {
openViewMap(mainUIController, [0, 0, MAIN_WIDTH, MAIN_HEIGHT]);
});
}
@@ -37,6 +37,8 @@ export class BlockCacher<
width: number;
/** 区域高度 */
height: number;
/** 区域面积 */
area: number = 0;
/** 分块大小 */
blockSize: number;
/** 分块信息 */
@@ -116,6 +118,7 @@ export class BlockCacher<
restWidth: this.width % this.blockSize,
restHeight: this.height % this.blockSize
};
this.area = this.blockData.width * this.blockData.height;
this.emit('split');
}
@@ -303,7 +306,10 @@ export interface ICanvasCacheItem extends IBlockCacheable {
}
export class CanvasCacheItem implements ICanvasCacheItem {
constructor(public canvas: MotaOffscreenCanvas2D, public symbol: number) {}
constructor(
public canvas: MotaOffscreenCanvas2D,
public symbol: number
) {}
destroy(): void {}
}
@@ -1132,6 +1132,7 @@ export class Layer extends Container<ELayerEvent> {
const length = this.backImage.length;
const img = this.backImage[frame % length];
need.forEach(index => {
if (index >= this.block.area || index < 0) return;
const x = index % width;
const y = Math.floor(index / width);
const sx = x * blockSize;
@@ -11,3 +11,4 @@ export * from './settings';
export * from './statistics';
export * from './statusBar';
export * from './toolbar';
export * from './viewmap';
@@ -19,6 +19,8 @@ import {
ReplayingToolbar
} from './toolbar';
import { HeroSkill } from '@user/data-state';
import { openViewMap } from './viewmap';
import { mainUIController } from './controller';
export interface ILeftHeroStatus {
hp: number;
@@ -98,6 +100,10 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
return [per * (n + 1), keyY, void 0, void 0, 0.5, 0.5];
};
const viewMap = () => {
openViewMap(mainUIController, [0, 0, 840, 480]);
};
return () => {
return (
<container loc={p.loc} hidden={p.hidden}>
@@ -106,6 +112,7 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
loc={central(24)}
font={font1}
cursor="pointer"
onClick={viewMap}
></text>
<text text={s.lv} loc={central(54)} font={font1}></text>
<image image={hpIcon} loc={iconLoc(0)}></image>
@@ -332,8 +332,8 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
const createMaskGradient = (ctx: CanvasRenderingContext2D) => {
maskGradient = ctx.createLinearGradient(100, 100, 200, 0);
maskGradient.addColorStop(0, '#fff');
maskGradient.addColorStop(1, '#000');
maskGradient.addColorStop(0, 'rgba(255,255,255,0)');
maskGradient.addColorStop(1, 'rgba(0,0,0,1)');
};
const createTitleGradient = (ctx: CanvasRenderingContext2D) => {
@@ -0,0 +1,503 @@
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
});
}