import type { GeneratedContents, RollTableResult } from '../../common/rolltable.js'; import { type FinalGeneratedContents, type GeneratedState, type InProgressGeneratedContents, type RollTableResultFull } from '../../common/rolltable.js'; import { ButtonStyle, type ComponentActionRow, type ComponentButton, type ComponentSelectMenu, type ComponentSelectOption, ComponentType, type EmbedAuthorOptions, EmbedField, MessageEmbed, type MessageEmbedOptions, type MessageOptions } from 'slash-create/web'; import markdownEscape from 'markdown-escape'; import type { EmbedFooterOptions } from 'slash-create/web.js'; export const SCENARIO_COLOR = 0x15A3C7; export const SUCCESS_COLOR = 0x79AC78; export const WARNING_COLOR = 0xF8ED62; export const FAILURE_COLOR = 0xA70000; export const LOCK_SUFFIX = ' \u{1f512}'; export const UNLOCK_SUFFIX = ' \u{1f513}'; export const ROLL_SUFFIX = ' \u{1f3b2}' const suffixes = [[LOCK_SUFFIX, false], [UNLOCK_SUFFIX, true], [ROLL_SUFFIX, true]] as const export function generateAuthorForResult(result: RollTableResultFull): EmbedAuthorOptions|undefined { return result.author ? { name: `${result.author.relation} ${result.author.name}`, url: result.author.url ?? undefined } : undefined; } export function generateFooterForResult(result: RollTableResultFull): EmbedFooterOptions { return { text: `in ${result.set.name ? 'the' : 'a'} ${result.set.global ? 'global' : 'server-local'} response set${result.set.name ? ' ' + markdownEscape(result.set.name) : ''}` }; } export function generateFieldForResult(value: RollTableResult, selected?: boolean): EmbedField { return { name: markdownEscape(`${value.table.header}${typeof selected === 'boolean' ? selected ? ROLL_SUFFIX : LOCK_SUFFIX : ''}`), value: markdownEscape(value.text), }; } export function generateEmbedForResult(title: string, color: number, value: RollTableResultFull): MessageEmbedOptions { return { title, color, author: generateAuthorForResult(value), fields: [generateFieldForResult(value)], timestamp: value.updated, footer: generateFooterForResult(value), } } export function generateEmbedForScenario(color: number, state: GeneratedState): MessageEmbedOptions { const fields: EmbedField[] = []; for (const value of state.rolled.values()) { fields.push(generateFieldForResult(value, state.final || !value.table.full ? undefined : state.selected.has(value.table))); } return { title: 'Your generated scenario', color, fields, timestamp: new Date() }; } export function getEmbedFrom({ embeds }: { embeds?: MessageEmbed[] }): MessageEmbed { const result = embeds && embeds.length >= 1 ? embeds[0] : null; if (!result) { throw Error('there were no embeds on the message to read'); } return result; } export function loadEmbed(embed: MessageEmbed, final: false): InProgressGeneratedContents export function loadEmbed(embed: MessageEmbed, final: true): FinalGeneratedContents export function loadEmbed(embed: MessageEmbed, final: boolean): GeneratedContents { const rolled = new Map() const selection = new Set() if (!embed.fields) { throw Error('there were no fields on the embed to read'); } for (const field of embed.fields) { let suffixInfo: readonly [string, boolean]|null = null for (const potentialSuffixInfo of suffixes) { if ((!suffixInfo || (potentialSuffixInfo[0].length > suffixInfo[0].length)) && field.name.endsWith(potentialSuffixInfo[0])) { suffixInfo = potentialSuffixInfo } } if (suffixInfo) { const [suffix, selected] = suffixInfo const name = field.name.substring(0, suffix ? field.name.length - suffix.length : undefined) rolled.set(name, field.value) if (selected) { selection.add(name) } } else { rolled.set(field.name, field.value) } } if (final) { return { final, rolled, } } else { return { final: false, rolled, selected: selection, } } } export const SELECT_ID = 'selected'; export const REROLL_ID = 'reroll'; export const DONE_ID = 'done'; export const DELETE_ID = 'delete'; export function generateActionsFor(state: GeneratedState): ComponentActionRow[] { if (state.final) { return []; } const selectOptions: ComponentSelectOption[] = Array.from(state.rolled.keys()).flatMap((v) => (v.full ? [{ default: state.selected.has(v), value: v.identifier, label: v.name, emoji: { name: v.emoji } }] : [])); if (selectOptions.length === 0) { return []; } const select: ComponentSelectMenu = { type: ComponentType.STRING_SELECT, custom_id: SELECT_ID, disabled: false, max_values: selectOptions.length, min_values: 0, options: selectOptions, placeholder: 'Components to reroll' }; const selectRow: ComponentActionRow = { type: ComponentType.ACTION_ROW, components: [select] }; const rerollButton: ComponentButton = { type: ComponentType.BUTTON, custom_id: REROLL_ID, disabled: state.selected.size === 0, emoji: { name: '\u{1f3b2}' }, label: (state.selected.size === 0 ? 'Reroll' : state.selected.size === state.rolled.size ? 'Reroll ALL' : 'Reroll Selected'), style: ButtonStyle.PRIMARY }; const doneButton: ComponentButton = { type: ComponentType.BUTTON, custom_id: DONE_ID, disabled: false, emoji: { name: '\u{1f44d}' }, label: 'Looks good!', style: ButtonStyle.SUCCESS }; const deleteButton: ComponentButton = { type: ComponentType.BUTTON, custom_id: DELETE_ID, disabled: false, emoji: { name: '\u{1f5d1}\ufe0f' }, label: 'Trash it.', style: ButtonStyle.DESTRUCTIVE }; const buttonRow: ComponentActionRow = { type: ComponentType.ACTION_ROW, components: [rerollButton, doneButton, deleteButton] }; return [selectRow, buttonRow]; } export function generateMessageFor(state: GeneratedState): MessageOptions { return { embeds: [generateEmbedForScenario(SCENARIO_COLOR, state)], components: generateActionsFor(state), ephemeral: false } } export function recordError(input: T): T & {message: string, stack: string} { const {error, context, extraData} = input const message = error instanceof Error ? error.message : `${error}` const stack = (error instanceof Error ? error.stack : null) ?? `${error}` console.error(`when trying to ${context ?? 'do something (unknown context)'}: ${stack}${extraData ? '\nExtra data: ' + extraData : ''}`) return {...input, message, stack} } export function generateErrorMessageFor(input: {error: unknown, context?: string, title?: string, extraData?: string}): MessageOptions { const {context, title, message} = recordError(input) return { embeds: [{ title: title ?? 'Error', description: `I wasn't able to ${markdownEscape(context ?? 'do that')}. Thing is, ${markdownEscape(message)}...`, color: FAILURE_COLOR, }], ephemeral: true }; }