import { DefaultProps, ElementLocator, Font, useKey } from '@motajs/render';
import { computed, defineComponent, reactive, ref } from 'vue';
import { Background, Selection } from './misc';
import { TextContent, TextContentExpose, TextContentProps } from './textbox';
import { SetupComponentOptions } from './types';
import { TextAlign } from './textboxTyper';
import { Page, PageExpose } from './page';
import { GameUI, IUIMountable } from '@motajs/system-ui';
export interface ConfirmBoxProps extends DefaultProps, TextContentProps {
text: string;
width: number;
loc: ElementLocator;
selFont?: Font;
selFill?: CanvasStyle;
pad?: number;
yesText?: string;
noText?: string;
winskin?: ImageIds;
defaultYes?: boolean;
color?: CanvasStyle;
border?: CanvasStyle;
}
export type ConfirmBoxEmits = {
yes: () => void;
no: () => void;
};
const confirmBoxProps = {
props: [
'text',
'width',
'loc',
'selFont',
'selFill',
'pad',
'yesText',
'noText',
'winskin',
'defaultYes',
'color',
'border'
],
emits: ['no', 'yes']
} satisfies SetupComponentOptions<
ConfirmBoxProps,
ConfirmBoxEmits,
keyof ConfirmBoxEmits
>;
/**
* 确认框组件,与 2.x 的 drawConfirm 类似,可以键盘操作,单次调用参考 {@link getConfirm}。
* 参数参考 {@link ConfirmBoxProps},事件参考 {@link ConfirmBoxEmits},用例如下:
* ```tsx
* const onYes = () => console.log('yes');
* const onNo = () => console.log('no');
*
*
* ```
*/
export const ConfirmBox = defineComponent<
ConfirmBoxProps,
ConfirmBoxEmits,
keyof ConfirmBoxEmits
>((props, { emit, attrs }) => {
const content = ref();
const height = ref(200);
const selected = ref(props.defaultYes ? true : false);
const yesSize = ref<[number, number]>([0, 0]);
const noSize = ref<[number, number]>([0, 0]);
const loc = computed(() => {
const [x = 0, y = 0, , , ax = 0, ay = 0] = props.loc;
return [x, y, props.width, height.value, ax, ay];
});
const yesText = computed(() => props.yesText ?? '确认');
const noText = computed(() => props.noText ?? '取消');
const pad = computed(() => props.pad ?? 32);
const yesLoc = computed(() => {
const y = height.value - pad.value;
return [props.width / 3, y, void 0, void 0, 0.5, 1];
});
const noLoc = computed(() => {
const y = height.value - pad.value;
return [(props.width / 3) * 2, y, void 0, void 0, 0.5, 1];
});
const contentLoc = computed(() => {
const width = props.width - pad.value * 2;
return [props.width / 2, pad.value, width, 0, 0.5, 0];
});
const selectLoc = computed(() => {
if (selected.value) {
const [x = 0, y = 0] = yesLoc.value;
const [width, height] = yesSize.value;
return [x, y + 4, width + 8, height + 8, 0.5, 1];
} else {
const [x = 0, y = 0] = noLoc.value;
const [width, height] = noSize.value;
return [x, y + 4, width + 8, height + 8, 0.5, 1];
}
});
const onUpdateHeight = (textHeight: number) => {
height.value = textHeight + pad.value * 4;
};
const setYes = (_: string, width: number, height: number) => {
yesSize.value = [width, height];
};
const setNo = (_: string, width: number, height: number) => {
noSize.value = [width, height];
};
const [key] = useKey();
key.realize('confirm', () => {
if (selected.value) emit('yes');
else emit('no');
});
key.realize('moveLeft', () => void (selected.value = true));
key.realize('moveRight', () => void (selected.value = false));
return () => (
emit('yes')}
onEnter={() => (selected.value = true)}
onSetText={setYes}
/>
emit('no')}
onEnter={() => (selected.value = false)}
onSetText={setNo}
/>
);
}, confirmBoxProps);
export type ChoiceKey = string | number | symbol;
export type ChoiceItem = [key: ChoiceKey, text: string];
export interface ChoicesProps extends DefaultProps, TextContentProps {
choices: ChoiceItem[];
loc: ElementLocator;
width: number;
maxHeight?: number;
text?: string;
title?: string;
winskin?: ImageIds;
color?: CanvasStyle;
border?: CanvasStyle;
selFont?: Font;
selFill?: CanvasStyle;
titleFont?: Font;
titleFill?: CanvasStyle;
pad?: number;
interval?: number;
}
export type ChoicesEmits = {
choose: (key: ChoiceKey) => void;
};
const choicesProps = {
props: [
'choices',
'loc',
'width',
'maxHeight',
'text',
'title',
'winskin',
'color',
'border',
'selFont',
'selFill',
'titleFont',
'titleFill',
'pad',
'interval'
],
emits: ['choose']
} satisfies SetupComponentOptions<
ChoicesProps,
ChoicesEmits,
keyof ChoicesEmits
>;
/**
* 选项框组件,用于在多个选项中选择一个,例如样板的系统设置就由它实现。单次调用参考 {@link getChoice}。
* 参数参考 {@link ChoicesProps},事件参考 {@link ChoicesEmits}。用例如下:
* ```tsx
* console.log(choice)}
* />
* ```
*/
export const Choices = defineComponent<
ChoicesProps,
ChoicesEmits,
keyof ChoicesEmits
>((props, { emit, attrs }) => {
const titleHeight = ref(0);
const contentHeight = ref(0);
const selected = ref(0);
const pageCom = ref();
const choiceSize = reactive<[number, number][]>([]);
const selFont = computed(() => props.selFont ?? new Font());
const maxHeight = computed(() => props.maxHeight ?? 360);
const pad = computed(() => props.pad ?? 28);
const choiceInterval = computed(() => props.interval ?? 16);
const hasText = computed(() => !!props.text);
const hasTitle = computed(() => !!props.title);
const contentWidth = computed(() => props.width - pad.value * 2);
const choiceHeight = computed(
() => selFont.value.size + 8 + choiceInterval.value
);
const contentY = computed(() => {
if (hasTitle.value) {
return pad.value * 2 + titleHeight.value;
} else {
return pad.value;
}
});
const choicesY = computed(() => {
const padding = pad.value;
const text = hasText.value;
let y = padding;
if (hasTitle.value) {
y += titleHeight.value;
if (text) {
y += padding / 2;
} else {
y += padding;
}
}
if (text) {
y += contentHeight.value;
y += padding / 2;
}
return y;
});
const choicesMaxHeight = computed(
() =>
maxHeight.value -
choicesY.value -
pad.value * 2 -
selFont.value.size -
8
);
const choiceCountPerPage = computed(() =>
Math.max(Math.floor(choicesMaxHeight.value / choiceHeight.value), 1)
);
const pages = computed(() =>
Math.ceil(props.choices.length / choiceCountPerPage.value)
);
const choicesHeight = computed(() => {
const padBottom = pages.value > 1 ? pad.value + selFont.value.size : 0;
if (props.choices.length > choiceCountPerPage.value) {
return choiceCountPerPage.value * choiceHeight.value + padBottom;
} else {
return props.choices.length * choiceHeight.value + padBottom;
}
});
const boxHeight = computed(() => {
if (props.choices.length > choiceCountPerPage.value) {
return (
choicesHeight.value +
choicesY.value +
// 不乘2是因为 choiceY 已经算上了顶部填充
pad.value
);
} else {
return (
choicesHeight.value +
choicesY.value +
// 不乘2是因为 choiceY 已经算上了顶部填充
pad.value
);
}
});
const boxLoc = computed(() => {
const [x = 0, y = 0, , , ax = 0, ay = 0] = props.loc;
return [x, y, props.width, boxHeight.value, ax, ay];
});
const titleLoc = computed(() => {
return [props.width / 2, pad.value, void 0, void 0, 0.5, 0];
});
const contentLoc = computed(() => {
return [
props.width / 2,
contentY.value,
contentWidth.value,
void 0,
0.5,
0
];
});
const choiceLoc = computed(() => {
return [
props.width / 2,
choicesY.value,
contentWidth.value,
choicesHeight.value,
0.5,
0
];
});
const selectionLoc = computed(() => {
const [width = 200, height = 200] = choiceSize[selected.value] ?? [];
return [
props.width / 2 - pad.value,
(selected.value + 0.5) * choiceHeight.value,
width + 8,
height + 8,
0.5,
0.5
];
});
const getPageContent = (page: number) => {
const count = choiceCountPerPage.value;
return props.choices.slice(page * count, (page + 1) * count);
};
const getChoiceLoc = (index: number): ElementLocator => {
return [
props.width / 2 - pad.value,
choiceHeight.value * (index + 0.5),
void 0,
void 0,
0.5,
0.5
];
};
const updateContentHeight = (height: number) => {
contentHeight.value = height;
};
const updateTitleHeight = (_0: string, _1: number, height: number) => {
titleHeight.value = height;
};
const updateChoiceSize = (index: number, width: number, height: number) => {
choiceSize[index] = [width, height];
};
const onPageChange = () => {
selected.value = 0;
};
const [key] = useKey();
key.realize('moveUp', () => {
if (selected.value === 0) {
if (pageCom.value?.now() !== 0) {
pageCom.value?.movePage(-1);
selected.value = choiceCountPerPage.value - 1;
}
} else {
selected.value--;
}
});
key.realize('moveDown', () => {
if (selected.value === choiceCountPerPage.value - 1) {
pageCom.value?.movePage(1);
selected.value = 0;
} else {
const page = pageCom.value?.now() ?? 1;
const index = page * choiceCountPerPage.value + selected.value;
if (index < props.choices.length - 1) {
selected.value++;
}
}
});
key.realize('moveLeft', () => pageCom.value?.movePage(-1));
key.realize('moveRight', () => pageCom.value?.movePage(1));
key.realize('confirm', () => {
const page = pageCom.value?.now() ?? 1;
const index = page * choiceCountPerPage.value + selected.value;
emit('choose', props.choices[index][0]);
});
return () => (
{hasTitle.value && (
)}
{hasText.value && (
)}
{(page: number) => [
,
...getPageContent(page).map((v, i) => {
return (
emit('choose', v[0])}
onSetText={(_, width, height) =>
updateChoiceSize(i, width, height)
}
onEnter={() => (selected.value = i)}
/>
);
})
]}
);
}, choicesProps);
/**
* 弹出一个确认框,然后将确认结果返回,例如给玩家弹出一个确认框,并获取玩家是否确认:
* ```ts
* const confirm = await getConfirm(
* // 在哪个 UI 控制器上打开,对于一般 UI 组件来说,直接填写 props.controller 即可
* props.controller,
* // 确认内容
* '确认要 xxx 吗?',
* // 确认框的位置,宽度由下一个参数指定,高度参数由组件内部计算得出,指定无效
* [240, 240, void 0, void 0, 0.5, 0.5],
* // 宽度设为 240
* 240,
* // 可以给选择框传入其他的 props,例如指定字体,此项可选
* { font: new Font('Verdana', 20) }
* );
* // 之后,就可以直接判断 confirm 来执行不同的操作了
* if (confirm) { ... }
* ```
* @param controller UI 控制器
* @param text 确认文本内容
* @param loc 确认框的位置
* @param width 确认框的宽度
* @param props 额外的 props,参考 {@link ConfirmBoxProps}
*/
export function getConfirm(
controller: IUIMountable,
text: string,
loc: ElementLocator,
width: number,
props?: Partial
) {
return new Promise(res => {
const instance = controller.open(
ConfirmBoxUI,
{
...(props ?? {}),
loc,
text,
width,
onYes: () => {
controller.close(instance);
res(true);
},
onNo: () => {
controller.close(instance);
res(false);
}
},
true
);
});
}
/**
* 弹出一个选择框,然后将选择结果返回,例如给玩家弹出一个选择框,并获取玩家选择了哪个:
* ```ts
* const choice = await getChoice(
* // 在哪个 UI 控制器上打开,对于一般 UI 组件来说,直接填写 props.controller 即可
* props.controller,
* // 选项内容,参考 Choices 的注释
* [[0, '选项1'], [1, '选项2'], [2, '选项3']],
* // 选择框的位置,宽度由下一个参数指定,高度参数由组件内部计算得出,指定无效
* [240, 240, void 0, void 0, 0.5, 0.5],
* // 宽度设为 240
* 240,
* // 可以给选择框传入其他的 props,例如指定标题,此项可选
* { title: '选项标题' }
* );
* // 之后,就可以直接判断 choice 来执行不同的操作了
* if (choice === 0) { ... }
* ```
* @param controller UI 控制器
* @param choices 选择框的选项
* @param loc 选择框的位置
* @param width 选择框的宽度
* @param props 额外的 props,参考 {@link ChoicesProps}
*/
export function getChoice(
controller: IUIMountable,
choices: ChoiceItem[],
loc: ElementLocator,
width: number,
props?: Partial
) {
return new Promise(res => {
const instance = controller.open(
ChoicesUI,
{
...(props ?? {}),
choices,
loc,
width,
onChoose: key => {
controller.close(instance);
res(key as T);
}
},
true
);
});
}
/** @see {@link ConfirmBox} */
export const ConfirmBoxUI = new GameUI('confirm-box', ConfirmBox);
/** @see {@link Choices} */
export const ChoicesUI = new GameUI('choices', Choices);