feat: 工具栏 toolbar

This commit is contained in:
unanmed 2025-02-24 20:27:35 +08:00
parent 91e448fdcc
commit ea7ede3748
17 changed files with 1261 additions and 38 deletions

View File

@ -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);
};
////// 设置播放速度 //////

View File

@ -99,6 +99,7 @@ export class Focus<T = any> extends EventEmitter<FocusEvent<T>> {
this.emit('splice', []);
return;
}
if (!this.stack[index]) return;
const last = this.stack.at(-1) ?? null;
if (!last) this.unfocus();
else this.focus(last);

View File

@ -941,10 +941,17 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
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<E extends ERenderItemEvent = ERenderItemEvent>
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) {

View File

@ -105,8 +105,8 @@ export abstract class GraphicItemBase
implements Required<ILineProperty>
{
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;
}

View File

@ -6,3 +6,4 @@ export * from './hero';
export * from './layer';
export * from './misc';
export * from './viewport';
export * from './graphics';

View File

@ -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)

View File

@ -92,6 +92,7 @@ export const { createApp, render } = createRenderer<RenderItem, RenderItem>({
nextSibling: function (
node: RenderItem<ERenderItemEvent>
): RenderItem<ERenderItemEvent> | null {
if (!node) return null;
if (!node.parent) {
return null;
} else {

View File

@ -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<ILineProperty> {
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]`

View File

@ -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, string>([
[Skill.Blade, '断灭之刃'],
[Skill.Shield, '铸剑为盾'],
[Skill.Jump, '跳跃']
]);
const skillDesc = new Map<Skill, (level: number) => 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;
}

View File

@ -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<GraphicPropsBase> {
loc: ElementLocator;
}
const iconsProps = {
props: ['loc']
} satisfies SetupComponentOptions<IconsProps>;
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<Path2D | undefined>,
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<IconsProps>(props => {
const path = ref<Path2D>();
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 (
<g-path
loc={props.loc}
path={path.value}
stroke
lineJoin="round"
lineCap="round"
></g-path>
);
};
}, iconsProps);
export const RetweetIcon = defineComponent<IconsProps>(props => {
const path = ref<Path2D>();
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 (
<g-path
loc={props.loc}
path={path.value}
stroke
lineJoin="round"
lineCap="round"
></g-path>
);
};
}, iconsProps);
export const ViewMapIcon = defineComponent<IconsProps>(props => {
const path = ref<Path2D>();
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 (
<g-path
loc={props.loc}
path={path.value}
stroke
lineJoin="round"
lineCap="round"
></g-path>
);
};
}, iconsProps);
export const DanmakuIcon = defineComponent<IconsProps>(props => {
const path = ref<Path2D>();
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 (
<g-path
loc={props.loc}
path={path.value}
stroke
lineJoin="round"
lineCap="round"
></g-path>
);
};
}, iconsProps);
export const ReplayIcon = defineComponent<IconsProps>(props => {
const path = ref<Path2D>();
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 (
<g-path
loc={props.loc}
path={path.value}
stroke
lineJoin="round"
lineCap="round"
></g-path>
);
};
}, iconsProps);
export const NumpadIcon = defineComponent<IconsProps>(props => {
const path = ref<Path2D>();
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 (
<g-path
loc={props.loc}
path={path.value}
stroke
lineJoin="round"
lineCap="round"
></g-path>
);
};
}, iconsProps);
export const PlayIcon = defineComponent<IconsProps>(props => {
const path = ref<Path2D>();
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 (
<g-path
loc={props.loc}
path={path.value}
fill
stroke
lineJoin="round"
lineCap="round"
></g-path>
);
};
}, iconsProps);
export const PauseIcon = defineComponent<IconsProps>(props => {
const path = ref<Path2D>();
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 (
<g-path
loc={props.loc}
path={path.value}
stroke
lineJoin="round"
lineCap="round"
></g-path>
);
};
}, iconsProps);
export const DoubleArrow = defineComponent<IconsProps>(props => {
const path = ref<Path2D>();
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 (
<g-path
loc={props.loc}
path={path.value}
stroke
fill
lineJoin="round"
lineCap="round"
></g-path>
);
};
}, iconsProps);
export const StepForward = defineComponent<IconsProps>(props => {
const path = ref<Path2D>();
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 (
<g-path
loc={props.loc}
path={path.value}
stroke
fill
lineJoin="round"
lineCap="round"
></g-path>
);
};
}, iconsProps);

View File

@ -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<ProgressProps>(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, () => {

View File

@ -211,7 +211,7 @@ const textboxOptions = {
'hidden',
'anchorX',
'anchorY',
'antiAliasing',
'anti',
'cache',
'composite',
'fall',

View File

@ -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}
></LeftStatusBar>
)}
<g-line line={[180, 0, 180, 480]} lineWidth={1} />
<container id="map-draw" {...mapDrawProps} x={180} zIndex={10}>
<layer-group id="layer-main" ex={layerGroupExtends} ref={map}>
<layer layer="bg" zIndex={10}></layer>
@ -141,6 +186,7 @@ const MainScene = defineComponent(() => {
<Textbox id="main-textbox" {...mainTextboxProps}></Textbox>
<FloorChange id="floor-change" zIndex={50}></FloorChange>
</container>
<g-line line={[180 + 480, 0, 180 + 480, 480]} lineWidth={1} />
{loaded.value && (
<RightStatusBar
loc={[480 + 180, 0, STATUS_BAR_WIDTH, STATUS_BAR_HEIGHT]}
@ -153,6 +199,12 @@ const MainScene = defineComponent(() => {
>
{mainUIController.render()}
</container>
<g-rect
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
zIndex={100}
stroke
noevent
></g-rect>
</container>
);
});

View File

@ -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<T> {
loc: ElementLocator;
status: T;
@ -47,7 +55,7 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
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<StatusBarProps<ILeftHeroStatus>>(
return () => {
return (
<container loc={p.loc}>
<g-rect loc={[0, 0, p.loc[2], p.loc[3]]} stroke></g-rect>
<text
text={floorName}
text={floorName.value}
loc={central(24)}
font={font1}
cursor="pointer"
@ -163,13 +170,228 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
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<StatusBarProps<IRightHeroStatus>>(
p => {
const font1 = '18px normal';
const font2 = '16px normal';
const minimap = ref<Sprite>();
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<RightStatusBarMisc[]>(() => {
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 (
<container loc={p.loc}>
<g-rect loc={[0, 0, p.loc[2], p.loc[3]]} stroke></g-rect>
<Scroll loc={[0, 0, 180, 100]}></Scroll>
<g-rectr
loc={[10, 10, 160, 24]}
circle={[6]}
fillStyle={skillColor.ref.value}
onClick={changeAutoSkill}
cursor="pointer"
></g-rectr>
<text
loc={central(22)}
text={skill.value}
font={font1}
onClick={changeAutoSkill}
cursor="pointer"
/>
<TextContent
loc={[10, 42, 160, 60]}
text={skillDesc.value}
fontFamily="normal"
fontSize={14}
width={160}
lineHeight={4}
></TextContent>
<g-line
line={[0, 107, 180, 107]}
strokeStyle="#888"
lineWidth={1}
zIndex={-20}
></g-line>
<Scroll loc={[0, 107, 180, 100]}>
{miscData.value
.map((v, i) => {
return [
<text
text={v.name}
loc={middle(10, 16 + i * 22)}
fillStyle={v.nameColor}
font={font2}
></text>,
<text
text={v.value}
loc={middle(100, 16 + i * 22)}
fillStyle={v.valueColor}
font={font2}
></text>
];
})
.flat()}
</Scroll>
<g-line
line={[0, 207, 180, 207]}
strokeStyle="#888"
lineWidth={1}
zIndex={-20}
></g-line>
<sprite
ref={minimap}
loc={[10, 207, 160, 160]}
render={drawMinimap}
></sprite>
<g-line
line={[0, 367, 180, 367]}
strokeStyle="#888"
lineWidth={1}
zIndex={-20}
></g-line>
{inNumpad.value ? (
<NumpadToolbar
loc={[0, 367, 180, 113]}
onNumpad={onNumpad}
/>
) : s.replaying ? (
<ReplayingToolbar
loc={[0, 367, 180, 113]}
status={s.replayStatus}
/>
) : (
<PlayingToolbar
loc={[0, 367, 180, 113]}
onNumpad={onNumpad}
/>
)}
</container>
);
};

View File

@ -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 () => (
<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} />
<DanmakuIcon loc={ic(2, 2)} strokeStyle="#eee" onClick={danmaku} />
<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 {
/** 是否正在播放 */
playing: boolean;
/** 录像播放速度 */
speed: number;
/** 已播放的长度 */
played: number;
/** 总长度 */
total: number;
}
export interface ReplayingProps extends ToolbarProps {
/** 录像播放状态 */
status: ReplayingStatus;
}
const replayingProps = {
props: ['status', 'loc']
} satisfies SetupComponentOptions<ReplayingProps>;
export const ReplayingToolbar = defineComponent<ReplayingProps>(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 (
<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 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 () => (
<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)}
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}
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}
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}
noevent
/>
<NumpadIcon loc={ic(0, 2)} strokeStyle="gold" onClick={numpad} />
</container>
);
}, toolbarProps);

View File

@ -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;
});
}

View File

@ -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;