diff --git a/public/libs/control.js b/public/libs/control.js index ce62750..a3254ec 100644 --- a/public/libs/control.js +++ b/public/libs/control.js @@ -1298,9 +1298,9 @@ control.prototype.triggerReplay = function () { control.prototype.pauseReplay = function () { if (!core.isPlaying() || !core.isReplaying()) return; core.status.replay.pausing = true; - core.updateStatusBar(false, true); core.drawTip('暂停播放'); Mota.require('var', 'hook').emit('replayStatus', false); + core.updateStatusBar(false, true); }; ////// 恢复播放 ////// @@ -1311,10 +1311,10 @@ control.prototype.resumeReplay = function () { return core.drawTip('请等待当前事件的处理结束'); } core.status.replay.pausing = false; - core.updateStatusBar(false, true); core.drawTip('恢复播放'); core.replay(); Mota.require('var', 'hook').emit('replayStatus', true); + core.updateStatusBar(false, true); }; ////// 单步播放 ////// @@ -1341,7 +1341,7 @@ control.prototype.speedUpReplay = function () { break; } } - core.drawTip('x' + core.status.replay.speed + '倍'); + core.updateStatusBar(false, true); }; ////// 减速播放 ////// @@ -1354,7 +1354,7 @@ control.prototype.speedDownReplay = function () { break; } } - core.drawTip('x' + core.status.replay.speed + '倍'); + core.updateStatusBar(false, true); }; ////// 设置播放速度 ////// diff --git a/src/core/main/custom/ui.ts b/src/core/main/custom/ui.ts index bacfbde..435f419 100644 --- a/src/core/main/custom/ui.ts +++ b/src/core/main/custom/ui.ts @@ -99,6 +99,7 @@ export class Focus extends EventEmitter> { this.emit('splice', []); return; } + if (!this.stack[index]) return; const last = this.stack.at(-1) ?? null; if (!last) this.unfocus(); else this.focus(last); diff --git a/src/core/render/item.ts b/src/core/render/item.ts index 78a4625..121ccb0 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -941,10 +941,17 @@ export abstract class RenderItem event: IActionEvent, transform: Transform ): vec3 { - const x = event.offsetX + this.anchorX * this.width; - const y = event.offsetY + this.anchorY * this.height; - if (this.type === 'absolute') return [x, y, 0]; - else return transform.untransformed(x, y); + const ax = this.anchorX * this.width; + const ay = this.anchorY * this.height; + if (this.type === 'absolute') { + return [event.offsetX + ax, event.offsetY + ay, 0]; + } else { + const [tx, ty] = transform.untransformed( + event.offsetX, + event.offsetY + ); + return [tx + ax, ty + ay, 0]; + } } /** @@ -1187,6 +1194,11 @@ export abstract class RenderItem this._transform.setRotate(nextValue); return; } + case 'noevent': { + if (!this.assertType(nextValue, 'boolean', key)) return; + this.noEvent = nextValue; + return; + } } const ev = this.parseEvent(key); if (ev) { diff --git a/src/core/render/preset/graphics.ts b/src/core/render/preset/graphics.ts index 27b25d2..03271cf 100644 --- a/src/core/render/preset/graphics.ts +++ b/src/core/render/preset/graphics.ts @@ -105,8 +105,8 @@ export abstract class GraphicItemBase implements Required { mode: GraphicMode = GraphicMode.Fill; - fill: CanvasStyle = '#fff'; - stroke: CanvasStyle = '#fff'; + fill: CanvasStyle = '#ddd'; + stroke: CanvasStyle = '#ddd'; lineWidth: number = 2; lineDash: number[] = []; lineDashOffset: number = 0; @@ -120,6 +120,7 @@ export abstract class GraphicItemBase private strokeAndFill: boolean = false; private propFillSet: boolean = false; + private actionStroke: boolean = false; private cachePath?: Path2D; protected pathDirty: boolean = false; @@ -172,11 +173,13 @@ export abstract class GraphicItemBase ctx.lineCap = this.lineCap; ctx.lineJoin = this.lineJoin; ctx.setLineDash(this.lineDash); + if (this.actionStroke) { + return ctx.isPointInStroke(path, fixX, fixY); + } switch (this.mode) { case GraphicMode.Fill: return ctx.isPointInPath(path, fixX, fixY, this.fillRule); case GraphicMode.Stroke: - return ctx.isPointInStroke(path, fixX, fixY); case GraphicMode.FillAndStroke: case GraphicMode.StrokeAndFill: return ( @@ -351,6 +354,10 @@ export abstract class GraphicItemBase this.miterLimit = nextValue; this.update(); return true; + case 'actionStroke': + if (!this.assertType(nextValue, 'boolean', key)) return false; + this.actionStroke = nextValue; + return true; } return false; } diff --git a/src/core/render/preset/index.ts b/src/core/render/preset/index.ts index 1078520..915d9f7 100644 --- a/src/core/render/preset/index.ts +++ b/src/core/render/preset/index.ts @@ -6,3 +6,4 @@ export * from './hero'; export * from './layer'; export * from './misc'; export * from './viewport'; +export * from './graphics'; diff --git a/src/core/render/render.ts b/src/core/render/render.ts index f6fed43..56e0a28 100644 --- a/src/core/render/render.ts +++ b/src/core/render/render.ts @@ -97,6 +97,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { // 画布监听 const canvas = this.target.canvas; canvas.addEventListener('mousedown', ev => { + ev.preventDefault(); const mouse = this.getMouseType(ev); this.lastMouse = mouse; this.captureEvent( @@ -105,11 +106,13 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { ); }); canvas.addEventListener('mouseup', ev => { + ev.preventDefault(); const event = this.createMouseAction(ev, ActionType.Up); this.captureEvent(ActionType.Up, event); this.captureEvent(ActionType.Click, event); }); canvas.addEventListener('mousemove', ev => { + ev.preventDefault(); const event = this.createMouseAction( ev, ActionType.Move, @@ -131,6 +134,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { ); }); canvas.addEventListener('mouseleave', ev => { + ev.preventDefault(); this.hoveredElement.forEach(v => { v.emit('leave', this.createMouseActionBase(ev, v)); }); @@ -138,11 +142,13 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { this.beforeHovered.clear(); }); document.addEventListener('touchstart', ev => { + ev.preventDefault(); this.createTouchAction(ev, ActionType.Down).forEach(v => { this.captureEvent(ActionType.Down, v); }); }); document.addEventListener('touchend', ev => { + ev.preventDefault(); this.createTouchAction(ev, ActionType.Up).forEach(v => { this.captureEvent(ActionType.Up, v); this.captureEvent(ActionType.Click, v); @@ -150,12 +156,14 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { }); }); document.addEventListener('touchcancel', ev => { + ev.preventDefault(); this.createTouchAction(ev, ActionType.Up).forEach(v => { this.captureEvent(ActionType.Up, v); this.touchInfo.delete(v.identifier); }); }); document.addEventListener('touchmove', ev => { + ev.preventDefault(); this.createTouchAction(ev, ActionType.Move).forEach(v => { const touch = this.touchInfo.get(v.identifier); if (!touch) return; @@ -172,6 +180,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { }); }); canvas.addEventListener('wheel', ev => { + ev.preventDefault(); this.captureEvent( ActionType.Wheel, this.createWheelAction(ev, ActionType.Wheel) diff --git a/src/core/render/renderer/index.ts b/src/core/render/renderer/index.ts index 01b41d5..579ebc2 100644 --- a/src/core/render/renderer/index.ts +++ b/src/core/render/renderer/index.ts @@ -92,6 +92,7 @@ export const { createApp, render } = createRenderer({ nextSibling: function ( node: RenderItem ): RenderItem | null { + if (!node) return null; if (!node.parent) { return null; } else { diff --git a/src/core/render/renderer/props.ts b/src/core/render/renderer/props.ts index 5dbbd55..91d9852 100644 --- a/src/core/render/renderer/props.ts +++ b/src/core/render/renderer/props.ts @@ -78,6 +78,8 @@ export interface BaseProps { scale?: ElementScale; /** 旋转属性,单位弧度,是 transform 的简写属性之一 */ rotate?: number; + /** 这个元素是否不会触发任何交互事件(cursor 属性也会无效),当执行到此元素时,会下穿至下一个元素 */ + noevent?: boolean; } export interface SpriteProps extends BaseProps { @@ -155,6 +157,8 @@ export interface GraphicPropsBase extends BaseProps, Partial { fillStyle?: CanvasStyle; /** 描边样式 */ strokeStyle?: CanvasStyle; + /** 在交互时,是否只检查交互位置只在描边上,对 fill, stroke, strokeAndFill 均有效 */ + actionStroke?: boolean; } export interface RectProps extends GraphicPropsBase {} @@ -229,7 +233,7 @@ export interface RectRProps extends GraphicPropsBase { */ circle?: RectRCircleParams; /** - * 圆形圆角参数,可以填 `[rx1, ry1, rx2, ry2, rx3, ry3, rx4, ry4]`, + * 椭圆圆角参数,可以填 `[rx1, ry1, rx2, ry2, rx3, ry3, rx4, ry4]`, * 两两一组,后三组可选,填写不同数量下的表现: * - 1组:每个角都是 `[rx1, ry1]` 半径的椭圆 * - 2组:左上和右下是 `[rx1, ry1]` 半径的椭圆,右上和左下是 `[rx2, ry2]` 半径的椭圆 diff --git a/src/game/mechanism/misc.ts b/src/game/mechanism/misc.ts index abdfdbb..e59eefa 100644 --- a/src/game/mechanism/misc.ts +++ b/src/game/mechanism/misc.ts @@ -1,6 +1,7 @@ import { backDir, has } from '@/plugin/game/utils'; import { loading } from '../game'; import type { LayerDoorAnimate } from '@/core/render/preset/floor'; +import { getSkillLevel } from '@/plugin/game/skillTree'; /** * 一些零散机制的数据 @@ -62,6 +63,24 @@ export namespace HeroSkill { export const Shield = Skill.Shield; export const Jump = Skill.Jump; + const skillNameMap = new Map([ + [Skill.Blade, '断灭之刃'], + [Skill.Shield, '铸剑为盾'], + [Skill.Jump, '跳跃'] + ]); + + const skillDesc = new Map string>([ + [ + Skill.Blade, + level => `攻击上升 ${level * 10}%,防御下降 ${level * 10}%` + ], + [ + Skill.Shield, + level => `防御上升 ${level * 10}%,攻击下降 ${level * 10}%` + ], + [Skill.Jump, () => `跳过前方障碍,或踢走面前的怪物`] + ]); + interface SkillSave { autoSkill: boolean; learned: Skill[]; @@ -71,6 +90,29 @@ export namespace HeroSkill { let autoSkill = true; let enabled: Skill = Skill.None; + export function getLevel(skill: Skill = getEnabled()) { + switch (skill) { + case Blade: + return getSkillLevel(2); + case Jump: + return learned.has(Jump) ? 1 : 0; + case Shield: + return getSkillLevel(10); + } + return 0; + } + + export function getSkillName(skill: Skill = getEnabled()) { + return skillNameMap.get(skill) ?? '未开启技能'; + } + + export function getSkillDesc( + skill: Skill = getEnabled(), + level: number = getLevel() + ) { + return skillDesc.get(skill)?.(level) ?? ''; + } + export function setAutoSkill(auto: boolean) { autoSkill = auto; } diff --git a/src/module/render/components/icons.tsx b/src/module/render/components/icons.tsx new file mode 100644 index 0000000..862a5b6 --- /dev/null +++ b/src/module/render/components/icons.tsx @@ -0,0 +1,503 @@ +import { DefaultProps, ElementLocator, GraphicPropsBase } from '@/core/render'; +import { computed, defineComponent, onMounted, Ref, ref, watch } from 'vue'; +import { SetupComponentOptions } from './types'; + +export interface IconsProps extends DefaultProps { + loc: ElementLocator; +} + +const iconsProps = { + props: ['loc'] +} satisfies SetupComponentOptions; + +type PathGenerator = (width: number, height: number) => Path2D; +/** + * @param ox 横向偏移坐标 + * @param oy 纵向偏移坐标 + * @param width 路径宽度 + * @param height 路径高度 + */ +type PathFn = (ox: number, oy: number, width: number, height: number) => Path2D; + +function adjustPath( + aspect: number, + ref: Ref, + fn: PathFn +): PathGenerator { + let beforeWidth = 200; + let beforeHeight = 200; + let path: Path2D | undefined; + return (width, height) => { + if (width === beforeWidth && height === beforeHeight && path) { + return path; + } + beforeWidth = width; + beforeHeight = height; + const eleAspect = width / height; + let ox = 0; + let oy = 0; + let dw = 0; + let dh = 0; + if (aspect >= eleAspect) { + ox = 0; + dw = width; + dh = width / aspect; + oy = (height - dh) / 2; + } else { + oy = 0; + dh = height; + dw = height * aspect; + ox = (width - dw) / 2; + } + path = fn(ox, oy, dw, dh); + ref.value = path; + return path; + }; +} + +export const RollbackIcon = defineComponent(props => { + const path = ref(); + + const width = computed(() => props.loc[2] ?? 200); + const height = computed(() => props.loc[3] ?? 200); + const generatePath = adjustPath(1, path, (ox, oy, width, height) => { + const path = new Path2D(); + const arc = width / 10; + const arrow = width / 10; + const left = ox + width / 10; + const top = oy + height / 5; + const right = ox + width - width / 10; + const bottom = oy + height - height / 5; + const end = left + width / 4; + path.moveTo(left, bottom); + path.lineTo(right - arc, bottom); + path.arcTo(right, bottom, right, bottom - arc, arc); + path.lineTo(right, top + arc); + path.arcTo(right, top, right - arc, top, arc); + path.lineTo(end, top); + path.moveTo(end + arrow, top - arrow); + path.lineTo(end, top); + path.lineTo(end + arrow, top + arrow); + path.moveTo(left, top); + return path; + }); + + watch(props, () => { + generatePath(width.value, height.value); + }); + + onMounted(() => { + generatePath(width.value, height.value); + }); + + return () => { + return ( + + ); + }; +}, iconsProps); + +export const RetweetIcon = defineComponent(props => { + const path = ref(); + + const width = computed(() => props.loc[2] ?? 200); + const height = computed(() => props.loc[3] ?? 200); + const generatePath = adjustPath(1, path, (ox, oy, width, height) => { + const path = new Path2D(); + const arc = width / 10; + const arrow = width / 10; + const left = ox + width / 10; + const top = oy + height / 5; + const right = ox + width - width / 10; + const bottom = oy + height - height / 5; + const end = left + width / 2; + path.moveTo(end, bottom); + path.lineTo(left + arc, bottom); + path.arcTo(left, bottom, left, bottom - arc, arc); + path.lineTo(left, top + arc); + path.arcTo(left, top, left + arc, top, arc); + path.lineTo(right, top); + path.moveTo(right - arrow, top - arrow); + path.lineTo(right, top); + path.lineTo(right - arrow, top + arrow); + path.moveTo(left, top); + return path; + }); + + watch(props, () => { + generatePath(width.value, height.value); + }); + + onMounted(() => { + generatePath(width.value, height.value); + }); + + return () => { + return ( + + ); + }; +}, iconsProps); + +export const ViewMapIcon = defineComponent(props => { + const path = ref(); + + const width = computed(() => props.loc[2] ?? 200); + const height = computed(() => props.loc[3] ?? 200); + const generatePath = adjustPath(1, path, (ox, oy, width, height) => { + const path = new Path2D(); + const left = ox + width / 5; + const top = oy + height / 5; + const right = ox + width - width / 5; + const bottom = oy + height - height / 5; + const cx = ox + width / 2; + const cy = oy + height / 2; + path.rect(left, top, right - left, bottom - top); + path.moveTo(cx, top); + path.lineTo(cx, bottom); + path.moveTo(left, cy); + path.lineTo(right, cy); + + return path; + }); + + watch(props, () => { + generatePath(width.value, height.value); + }); + + onMounted(() => { + generatePath(width.value, height.value); + }); + + return () => { + return ( + + ); + }; +}, iconsProps); + +export const DanmakuIcon = defineComponent(props => { + const path = ref(); + + const width = computed(() => props.loc[2] ?? 200); + const height = computed(() => props.loc[3] ?? 200); + const generatePath = adjustPath(1, path, (ox, oy, width, height) => { + const path = new Path2D(); + const left = ox + width / 5; + const bottom = oy + height - height / 5; + const cx = ox + width / 2; + const cy = oy + height / 2; + const rx = width / 3; + const ry = height / 4; + const start = (Math.PI * 16) / 18; + const end = (Math.PI * 12) / 18; + path.ellipse(cx, cy, rx, ry, 0, start, end); + path.lineTo(left - width / 24, bottom - height / 36); + path.closePath(); + return path; + }); + + watch(props, () => { + generatePath(width.value, height.value); + }); + + onMounted(() => { + generatePath(width.value, height.value); + }); + + return () => { + return ( + + ); + }; +}, iconsProps); + +export const ReplayIcon = defineComponent(props => { + const path = ref(); + + const width = computed(() => props.loc[2] ?? 200); + const height = computed(() => props.loc[3] ?? 200); + const generatePath = adjustPath(1, path, (ox, oy, width, height) => { + const path = new Path2D(); + const arc = width / 10; + const left = ox + width / 5; + const top = oy + height / 5; + const right = ox + width - width / 5; + const bottom = oy + height - height / 5; + const cy = oy + height / 2; + path.moveTo(right, cy - height / 8); + path.lineTo(right, top + arc); + path.arcTo(right, top, right - arc, top, arc); + path.lineTo(left + arc, top); + path.arcTo(left, top, left, top + arc, arc); + path.lineTo(left, cy); + path.moveTo(left + arc, cy - arc); + path.lineTo(left, cy); + path.lineTo(left - arc, cy - arc); + path.moveTo(left, cy + height / 8); + path.lineTo(left, bottom - arc); + path.arcTo(left, bottom, left + arc, bottom, arc); + path.lineTo(right - arc, bottom); + path.arcTo(right, bottom, right, bottom - arc, arc); + path.lineTo(right, cy); + path.moveTo(right - arc, cy + arc); + path.lineTo(right, cy); + path.lineTo(right + arc, cy + arc); + return path; + }); + + watch(props, () => { + generatePath(width.value, height.value); + }); + + onMounted(() => { + generatePath(width.value, height.value); + }); + + return () => { + return ( + + ); + }; +}, iconsProps); + +export const NumpadIcon = defineComponent(props => { + const path = ref(); + + const width = computed(() => props.loc[2] ?? 200); + const height = computed(() => props.loc[3] ?? 200); + const generatePath = adjustPath(1, path, (ox, oy, width, height) => { + const path = new Path2D(); + const left = ox + width / 5; + const top = oy + height / 5; + const right = ox + width - width / 5; + const bottom = oy + height - height / 5; + const cx = ox + width / 2; + const cy = oy + height / 2; + path.rect(left, top, right - left, bottom - top); + const path2 = new Path2D(); + path2.ellipse(cx, cy, width / 9, height / 6, 0, 0, Math.PI * 2); + path.addPath(path2); + return path; + }); + + watch(props, () => { + generatePath(width.value, height.value); + }); + + onMounted(() => { + generatePath(width.value, height.value); + }); + + return () => { + return ( + + ); + }; +}, iconsProps); + +export const PlayIcon = defineComponent(props => { + const path = ref(); + + const width = computed(() => props.loc[2] ?? 200); + const height = computed(() => props.loc[3] ?? 200); + const generatePath = adjustPath(1, path, (ox, oy, width, height) => { + const path = new Path2D(); + const left = ox + width / 5; + const top = oy + height / 5; + const right = ox + width - width / 5; + const bottom = oy + height - height / 5; + path.moveTo(left, top); + path.lineTo(right, oy + height / 2); + path.lineTo(left, bottom); + path.closePath(); + return path; + }); + + watch(props, () => { + generatePath(width.value, height.value); + }); + + onMounted(() => { + generatePath(width.value, height.value); + }); + + return () => { + return ( + + ); + }; +}, iconsProps); + +export const PauseIcon = defineComponent(props => { + const path = ref(); + + const width = computed(() => props.loc[2] ?? 200); + const height = computed(() => props.loc[3] ?? 200); + const generatePath = adjustPath(1, path, (ox, oy, width, height) => { + const path = new Path2D(); + const cx = ox + width / 2; + const top = oy + height / 5; + const bottom = oy + height - height / 5; + path.moveTo(cx - width / 5, top); + path.lineTo(cx - width / 5, bottom); + path.moveTo(cx + width / 5, top); + path.lineTo(cx + width / 5, bottom); + return path; + }); + + watch(props, () => { + generatePath(width.value, height.value); + }); + + onMounted(() => { + generatePath(width.value, height.value); + }); + + return () => { + return ( + + ); + }; +}, iconsProps); + +export const DoubleArrow = defineComponent(props => { + const path = ref(); + + const width = computed(() => props.loc[2] ?? 200); + const height = computed(() => props.loc[3] ?? 200); + const generatePath = adjustPath(1, path, (ox, oy, width, height) => { + const path = new Path2D(); + const path2 = new Path2D(); + const left = ox + width / 5; + const top = oy + height / 5; + const right = ox + width - width / 5; + const bottom = oy + height - height / 5; + const cx = ox + width / 2; + const cy = oy + height / 2; + path.moveTo(left, top + height / 12); + path.lineTo(cx + width / 8, cy); + path.lineTo(left, bottom - height / 12); + path.closePath(); + path2.moveTo(cx, top + height / 12); + path2.lineTo(right, cy); + path2.lineTo(cx, bottom - height / 12); + path2.closePath(); + path.addPath(path2); + return path; + }); + + watch(props, () => { + generatePath(width.value, height.value); + }); + + onMounted(() => { + generatePath(width.value, height.value); + }); + + return () => { + return ( + + ); + }; +}, iconsProps); + +export const StepForward = defineComponent(props => { + const path = ref(); + + const width = computed(() => props.loc[2] ?? 200); + const height = computed(() => props.loc[3] ?? 200); + const generatePath = adjustPath(1, path, (ox, oy, width, height) => { + const path = new Path2D(); + const path2 = new Path2D(); + const left = ox + width / 5; + const top = oy + height / 5; + const right = ox + width - width / 5; + const bottom = oy + height - height / 5; + path.moveTo(left, top); + path.lineTo(right, oy + height / 2); + path.lineTo(left, bottom); + path.closePath(); + path2.moveTo(right, top); + path2.lineTo(right, bottom); + path.addPath(path2); + return path; + }); + + watch(props, () => { + generatePath(width.value, height.value); + }); + + onMounted(() => { + generatePath(width.value, height.value); + }); + + return () => { + return ( + + ); + }; +}, iconsProps); diff --git a/src/module/render/components/misc.tsx b/src/module/render/components/misc.tsx index 6073e2d..d59f9f8 100644 --- a/src/module/render/components/misc.tsx +++ b/src/module/render/components/misc.tsx @@ -12,6 +12,8 @@ interface ProgressProps extends DefaultProps { success?: CanvasStyle; /** 未完成部分的样式,默认为灰色(gray) */ background?: CanvasStyle; + /** 线宽度 */ + lineWidth?: number; } const progressProps = { @@ -25,10 +27,22 @@ export const Progress = defineComponent(props => { const { ctx } = canvas; const width = props.loc[2] ?? 200; const height = props.loc[3] ?? 200; - ctx.fillStyle = props.background ?? 'gray'; - ctx.fillRect(0, 0, width, height); - ctx.fillStyle = props.success ?? 'green'; - ctx.fillRect(0, 0, width * props.progress, height); + ctx.lineCap = 'round'; + const lineWidth = props.lineWidth ?? 2; + ctx.lineWidth = lineWidth; + ctx.strokeStyle = props.background ?? 'gray'; + ctx.beginPath(); + ctx.moveTo(lineWidth, height / 2); + ctx.lineTo(width - lineWidth, height / 2); + ctx.stroke(); + if (!isNaN(props.progress)) { + ctx.strokeStyle = props.success ?? 'green'; + const p = lineWidth + (width - lineWidth * 2) * props.progress; + ctx.beginPath(); + ctx.moveTo(lineWidth, height / 2); + ctx.lineTo(p, height / 2); + ctx.stroke(); + } }; watch(props, () => { diff --git a/src/module/render/components/textbox.tsx b/src/module/render/components/textbox.tsx index 8754407..732a546 100644 --- a/src/module/render/components/textbox.tsx +++ b/src/module/render/components/textbox.tsx @@ -211,7 +211,7 @@ const textboxOptions = { 'hidden', 'anchorX', 'anchorY', - 'antiAliasing', + 'anti', 'cache', 'composite', 'fall', diff --git a/src/module/render/ui/main.tsx b/src/module/render/ui/main.tsx index 0576841..3131dce 100644 --- a/src/module/render/ui/main.tsx +++ b/src/module/render/ui/main.tsx @@ -33,6 +33,7 @@ import { RightStatusBar } from './statusBar'; import { onLoaded } from '../use'; +import { ReplayingStatus } from './toolbar'; const MainScene = defineComponent(() => { const layerGroupExtends: ILayerGroupRenderExtends[] = [ @@ -92,12 +93,29 @@ const MainScene = defineComponent(() => { exAtk: 0, magicDef: 0 }); - const rightStatus: IRightHeroStatus = reactive({}); + const replayStatus: ReplayingStatus = reactive({ + playing: false, + speed: 1, + played: 0, + total: 0 + }); + const rightStatus: IRightHeroStatus = reactive({ + autoSkill: false, + skillName: '', + skillDesc: '', + jumpCount: 0, + springCount: 0, + floor: 'MT0', + replaying: false, + replayStatus + }); const { getHeroStatusOn } = Mota.requireAll('fn'); const updateStatus = () => { + if (!core.status || !core.status.hero || !core.status.floorId) return; const hero = core.status.hero; + const floor = core.status.floorId; leftStatus.atk = getHeroStatusOn('atk'); leftStatus.hp = getHeroStatusOn('hp'); leftStatus.def = getHeroStatusOn('def'); @@ -112,6 +130,32 @@ const MainScene = defineComponent(() => { leftStatus.regen = getHeroStatusOn('hpmax'); leftStatus.exAtk = getHeroStatusOn('mana'); leftStatus.magicDef = getHeroStatusOn('magicDef'); + + const { HeroSkill } = Mota.require('module', 'Mechanism'); + rightStatus.autoSkill = HeroSkill.getAutoSkill(); + rightStatus.skillName = HeroSkill.getSkillName(); + rightStatus.skillDesc = HeroSkill.getSkillDesc(); + rightStatus.floor = floor; + rightStatus.replaying = core.isReplaying(); + const { pausing, speed, toReplay, totalList } = core.status.replay; + replayStatus.playing = !pausing; + replayStatus.speed = speed; + replayStatus.played = totalList.length - toReplay.length; + replayStatus.total = totalList.length; + if (HeroSkill.learnedSkill(HeroSkill.Jump)) { + if (Mota.Plugin.require('skill_g').jumpIgnoreFloor.has(floor)) { + rightStatus.jumpCount = -2; + } else { + rightStatus.jumpCount = 3 - (flags[`jump_${floor}`] ?? 0); + } + } else { + rightStatus.jumpCount = -1; + } + if (core.hasFlag('spring')) { + rightStatus.springCount = 50 - (flags.springCount ?? 0); + } else { + rightStatus.springCount = -1; + } }; const loaded = ref(false); @@ -129,6 +173,7 @@ const MainScene = defineComponent(() => { status={leftStatus} > )} + @@ -141,6 +186,7 @@ const MainScene = defineComponent(() => { + {loaded.value && ( { > {mainUIController.render()} + ); }); diff --git a/src/module/render/ui/statusBar.tsx b/src/module/render/ui/statusBar.tsx index 2b08506..6e536f0 100644 --- a/src/module/render/ui/statusBar.tsx +++ b/src/module/render/ui/statusBar.tsx @@ -1,8 +1,18 @@ import { GameUI } from '@/core/system'; -import { defineComponent } from 'vue'; -import { SetupComponentOptions } from '../components'; -import { ElementLocator } from '@/core/render'; +import { computed, defineComponent, ref, watch } from 'vue'; +import { SetupComponentOptions, TextContent } from '../components'; +import { ElementLocator, Sprite } from '@/core/render'; +import { transitionedColor } from '../use'; +import { linear } from 'mutate-animate'; import { Scroll } from '../components/scroll'; +import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; +import { getArea, MinimapDrawer } from '@/plugin/ui/fly'; +import { + NumpadToolbar, + PlayingToolbar, + ReplayingStatus, + ReplayingToolbar +} from './toolbar'; export interface ILeftHeroStatus { hp: number; @@ -24,8 +34,6 @@ export interface ILeftHeroStatus { magicDef: number; } -export interface IRightHeroStatus {} - interface StatusBarProps { loc: ElementLocator; status: T; @@ -47,7 +55,7 @@ export const LeftStatusBar = defineComponent>( const s = p.status; const f = core.formatBigNumber; - const floorName = core.floors[s.floor].title; + const floorName = computed(() => core.floors[s.floor]?.title ?? ''); const key = (num: number) => { return num.toString().padStart(2, '0'); @@ -86,9 +94,8 @@ export const LeftStatusBar = defineComponent>( return () => { return ( - >( statusBarProps ); +interface RightStatusBarMisc { + name: string; + value: string; + nameColor: string; + valueColor: string; +} + +export interface IRightHeroStatus { + /** 自动切换技能 */ + autoSkill: boolean; + /** 当前开启的技能 */ + skillName: string; + /** 技能描述 */ + skillDesc: string; + /** 跳跃剩余次数,-1 表示未开启,-2表示当前楼层不能跳 */ + jumpCount: number; + /** 治愈之泉剩余次数,-1 表示未开启 */ + springCount: number; + /** 当前楼层 */ + floor: FloorIds; + /** 是否正在录像播放 */ + replaying: boolean; + /** 录像播放状态 */ + replayStatus: ReplayingStatus; +} + export const RightStatusBar = defineComponent>( p => { + const font1 = '18px normal'; + const font2 = '16px normal'; + + const minimap = ref(); + const inNumpad = ref(false); + + const onNumpad = () => { + inNumpad.value = !inNumpad.value; + }; + + const s = p.status; + const skill = computed(() => + s.autoSkill ? '已开启自动切换' : s.skillName + ); + const skillDesc = computed(() => + s.autoSkill ? '自动切换技能时,会自动选择最优技能' : s.skillDesc + ); + + const skillColor = transitionedColor('#284', 200, linear())!; + + watch( + () => s.autoSkill, + value => { + skillColor.set(value ? '#284' : '#824'); + } + ); + + const miscData = computed(() => { + const data: RightStatusBarMisc[] = []; + if (s.jumpCount !== -1) { + const text = + s.jumpCount === -2 ? '不可跳跃' : s.jumpCount.toString(); + data.push({ + name: '跳跃剩余', + nameColor: '#fff', + value: text, + valueColor: '#fff' + }); + } + if (s.springCount >= 0) { + data.push({ + name: '治愈之泉', + nameColor: '#a7ffa7', + value: s.springCount.toString(), + valueColor: '#a7ffa7' + }); + } + return data; + }); + + const central = (y: number): ElementLocator => { + const width = p.loc[2] ?? 200; + return [width / 2, y, void 0, void 0, 0.5, 0.5]; + }; + + const middle = (x: number, y: number): ElementLocator => { + return [x, y, void 0, void 0, 0, 0.5]; + }; + + const changeAutoSkill = () => { + const { HeroSkill } = Mota.require('module', 'Mechanism'); + const auto = !s.autoSkill; + HeroSkill.setAutoSkill(auto); + core.status.route.push(`set:autoSkill:${auto}`); + core.updateStatusBar(); + }; + + const area = getArea(); + const minimapDrawer = new MinimapDrawer( + document.createElement('canvas') + ); + minimapDrawer.noBorder = true; + minimapDrawer.scale = 4; + minimapDrawer.showInfo = true; + let linked = false; + const drawMinimap = (canvas: MotaOffscreenCanvas2D) => { + const ctx = canvas.ctx; + ctx.save(); + ctx.scale( + 1 / core.domStyle.scale / devicePixelRatio, + 1 / core.domStyle.scale / devicePixelRatio + ); + if (!linked) { + minimapDrawer.link(canvas.canvas); + linked = true; + } + if (minimapDrawer.nowFloor !== s.floor) { + minimapDrawer.drawedThumbnail = {}; + } else { + minimapDrawer.drawToTarget(); + ctx.restore(); + return; + } + minimapDrawer.nowFloor = s.floor; + minimapDrawer.nowArea = + Object.keys(area).find(v => + area[v].includes(core.status.floorId) + ) ?? ''; + minimapDrawer.locateMap(minimapDrawer.nowFloor); + minimapDrawer.drawMap(); + ctx.restore(); + }; + + watch( + () => s.floor, + () => { + minimap.value?.update(); + } + ); + return () => { return ( - - + + + + + + {miscData.value + .map((v, i) => { + return [ + , + + ]; + }) + .flat()} + + + + + {inNumpad.value ? ( + + ) : s.replaying ? ( + + ) : ( + + )} ); }; diff --git a/src/module/render/ui/toolbar.tsx b/src/module/render/ui/toolbar.tsx new file mode 100644 index 0000000..5663450 --- /dev/null +++ b/src/module/render/ui/toolbar.tsx @@ -0,0 +1,364 @@ +import { DefaultProps, ElementLocator } from '@/core/render'; +import { computed, defineComponent, ref } from 'vue'; +import { SetupComponentOptions } from '../components'; +import { + DanmakuIcon, + DoubleArrow, + NumpadIcon, + PauseIcon, + PlayIcon, + ReplayIcon, + RetweetIcon, + RollbackIcon, + StepForward, + ViewMapIcon +} from '../components/icons'; +import { + generateBinary, + getVitualKeyOnce, + openDanmakuPoster +} from '@/plugin/utils'; +import { gameKey } from '@/core/main/custom/hotkey'; +import { generateKeyboardEvent } from '@/core/main/custom/keyboard'; +import { transitioned } from '../use'; +import { linear } from 'mutate-animate'; +import { KeyCode } from '@/plugin/keyCodes'; +import { Progress } from '../components/misc'; + +interface ToolbarProps extends DefaultProps { + loc?: ElementLocator; +} + +type ToolbarEmits = { + numpad: () => void; +}; + +const toolbarProps = { + props: ['loc'], + 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 bookIcon = core.statusBar.icons.book; + const flyIcon = core.statusBar.icons.fly; + const toolIcon = core.statusBar.icons.toolbox; + const equipIcon = core.statusBar.icons.equipbox; + const keyIcon = core.statusBar.icons.keyboard; + const shopIcon = core.statusBar.icons.shop; + const saveIcon = core.statusBar.icons.save; + const loadIcon = core.statusBar.icons.load; + const setIcon = core.statusBar.icons.settings; + + const iconFont = '12px Verdana'; + + const book = () => core.openBook(true); + const tool = () => core.openEquipbox(true); + const fly = () => core.useFly(true); + const save = () => core.save(true); + const load = () => core.load(true); + 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 = () => { + if (core.isPlaying() && !core.isMoving() && !core.status.lockControl) { + core.ui._drawViewMaps(); + } + }; + const danmaku = () => requestAnimationFrame(openDanmakuPoster); + const replay = () => core.ui._drawReplay(); + const settings = () => core.openSettings(true); + + return () => ( + + + + + + + + + + + + + + + + + + + ); +}, toolbarProps); + +export interface ReplayingStatus { + /** 是否正在播放 */ + playing: boolean; + /** 录像播放速度 */ + speed: number; + /** 已播放的长度 */ + played: number; + /** 总长度 */ + total: number; +} + +export interface ReplayingProps extends ToolbarProps { + /** 录像播放状态 */ + status: ReplayingStatus; +} + +const replayingProps = { + props: ['status', 'loc'] +} satisfies SetupComponentOptions; + +export const ReplayingToolbar = defineComponent(props => { + const status = props.status; + + const bookIcon = core.statusBar.icons.book; + const saveIcon = core.statusBar.icons.save; + const font1 = '16px normal'; + const font2 = '12px Verdana'; + + 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 = () => core.save(true); + const view = () => { + if (core.isPlaying() && !core.isMoving() && !core.status.lockControl) { + core.ui._drawViewMaps(); + } + }; + const rewind = () => core.rewindReplay(); + const step = () => core.stepReplay(); + + return () => { + return ( + + {status.playing ? ( + + ) : ( + + )} + + + + + + + + + + + + + + ); + }; +}, 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 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(255,255,255,${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(255,255,255,${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(255,255,255,${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 () => ( + + + {nums + .map((v, i) => { + const col = i % 5; + const row = Math.floor(i / 5); + return [ + clickNum(v)} + />, + + ]; + }) + .flat()} + + + + + + + + + + ); +}, toolbarProps); diff --git a/src/plugin/game/ui.ts b/src/plugin/game/ui.ts index d7cb4d2..849e680 100644 --- a/src/plugin/game/ui.ts +++ b/src/plugin/game/ui.ts @@ -28,8 +28,6 @@ export function init() { if (!core.control.noAutoEvents) core.checkAutoEvents(); core.control._updateStatusBar_setToolboxIcon(); core.control.noAutoEvents = true; - // 更新vue状态栏 - updateVueStatusBar(); Mota.require('var', 'hook').emit('statusBarUpdate'); }; @@ -66,10 +64,3 @@ export function init() { core.setFlag('showToolbox', showToolbox || null); }; } - -function updateVueStatusBar() { - Mota.r(() => { - const status = Mota.require('var', 'status'); - status.value = !status.value; - }); -} diff --git a/src/plugin/utils.ts b/src/plugin/utils.ts index fa947d5..f4b534e 100644 --- a/src/plugin/utils.ts +++ b/src/plugin/utils.ts @@ -392,7 +392,7 @@ export function generateBinary(arr: boolean[]) { let num = 0; arr.forEach((v, i) => { if (v) { - num += 1 << i; + num |= 1 << i; } }); return num;