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