import { type RollableTables, rollOn, RollTable, RollTableOrder } from './rolltable.js'; import { ButtonStyle, type ComponentActionRow, type ComponentButton, type ComponentSelectMenu, type ComponentSelectOption, ComponentType, EmbedField, MessageEmbed, type MessageEmbedOptions, type MessageOptions } from 'slash-create/web'; export type ComponentValues = { [key in RollTable]?: string } export type ComponentLocks = { [Key in RollTable]?: boolean } export interface GeneratedMessage { values: ComponentValues locked?: ComponentLocks } export const RollTableEmoji = { [RollTable.Setting]: '\u{1f3d9}\ufe0f', [RollTable.Theme]: '\u{1f4d4}', [RollTable.Start]: '\u25b6\ufe0f', [RollTable.Challenge]: '\u{1f613}', [RollTable.Twist]: '\u{1f500}', [RollTable.Focus]: '\u{1f444}', [RollTable.Word]: '\u{2728}' } as const satisfies {readonly [key in RollTable]: string} export const RollTableEmbedTitles = { [RollTable.Setting]: 'The action takes place...', [RollTable.Theme]: 'The encounter is themed around...', [RollTable.Start]: 'The action begins when...', [RollTable.Challenge]: 'Things are more difficult because...', [RollTable.Twist]: 'Partway through, unexpectedly...', [RollTable.Focus]: 'The vore scene is focused on...', [RollTable.Word]: 'The word of the day is...' } as const satisfies {readonly [key in RollTable]: string} export const RollTableNames = { [RollTable.Setting]: 'Setting', [RollTable.Theme]: 'Theme', [RollTable.Start]: 'Inciting Incident', [RollTable.Challenge]: 'Challenge', [RollTable.Twist]: 'Twist', [RollTable.Focus]: 'Vore Scene Focus', [RollTable.Word]: 'Word of the Day' } as const satisfies {readonly [key in RollTable]: string} export const RollTableEmbedsReversed = { "\u{1f3d9}\ufe0f The action takes place...": RollTable.Setting, "\u{1f4d4} The encounter is themed around...": RollTable.Theme, "\u25b6\ufe0f The action begins when...": RollTable.Start, "\u{1f613} Things are more difficult because...": RollTable.Challenge, "\u{1f500} Partway through, unexpectedly...": RollTable.Twist, "\u{1f444} The vore scene is focused on...": RollTable.Focus, "\u{2728} The word of the day is...": RollTable.Word, } as const satisfies {readonly [key in RollTable as `${typeof RollTableEmoji[key]} ${typeof RollTableEmbedTitles[key]}`]: key} & {[other: string]: RollTable} export function calculateUnlockedValues(original?: ComponentValues|undefined, locks?: ComponentLocks|undefined): RollTable[] { if (!original && !locks) { return RollTableOrder } const existingItems = original ? RollTableOrder.filter(v => typeof original[v] !== "undefined") : RollTableOrder return locks ? existingItems.filter(v => locks[v] !== true) : existingItems } export function generateValuesFor(selected: readonly RollTable[], tables: RollableTables, original: ComponentValues = {}): ComponentValues { const result: ComponentValues = Object.assign({}, original) for (const table of selected) { result[table] = rollOn(table, tables) } return result } export const LOCK_SUFFIX = " \u{1f512}" export const UNLOCK_SUFFIX = " \u{1f513}" export function generateFieldFor(field: RollTable, value: string, lock: boolean|null = null) { return { name: RollTableEmoji[field] + " " + RollTableEmbedTitles[field] + (lock !== null ? (lock ? LOCK_SUFFIX : UNLOCK_SUFFIX) : ""), value, } } export function generateEmbedFor(values: ComponentValues, locks: ComponentLocks|undefined): MessageEmbedOptions { const fields: EmbedField[] = [] const usableLocks = locks ?? {} for (const field of RollTableOrder) { const value = values[field] if (value) { fields.push(generateFieldFor(field, value, usableLocks.hasOwnProperty(field) ? usableLocks[field] : null)) } } return { title: 'Your generated scenario', fields, timestamp: new Date().toISOString() } } export function getEmbedFrom({embeds}: {embeds?: MessageEmbed[]|undefined}): 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): GeneratedMessage { const result: {values: ComponentValues, locked: ComponentLocks} = { values: {}, locked: {}, } if (!embed.fields || embed.fields.length === 0) { throw Error("there were no fields on the embed to read") } for (const field of embed.fields!) { let locked: boolean|undefined, name = field.name if (name.endsWith(LOCK_SUFFIX)) { locked = true name = name.substring(0, name.length - LOCK_SUFFIX.length) } else if (name.endsWith(UNLOCK_SUFFIX)) { locked = false name = name.substring(0, name.length - UNLOCK_SUFFIX.length) } else { throw Error(`there was no lock or unlock suffix on ${name}`) } const value = field.value if (RollTableEmbedsReversed.hasOwnProperty(name)) { const table = RollTableEmbedsReversed[name as keyof typeof RollTableEmbedsReversed] if (typeof locked !== "undefined") { result.locked[table] = locked } result.values[table] = value } else { throw Error(`I don't know a field named ${name}`) } } return result } export function populateLocksFor(values: ComponentValues, original?: ComponentLocks|undefined): ComponentLocks { const result = Object.assign({}, original) for (const table of RollTableOrder) { if (typeof values[table] !== "undefined") { result[table] = result[table] ?? true } } return result } export function selectUnlockedFrom(values: string[], oldLocks?: ComponentLocks | undefined): ComponentLocks { const result = Object.assign({}, oldLocks ?? {}) for (const table of RollTableOrder) { if (result.hasOwnProperty(table)) { result[table] = !values.includes(`${table}`) } } return result } export const SELECT_ID = "selected" export const REROLL_ID = "reroll" export const DONE_ID = "done" export const DELETE_ID = "delete" export function generateActionsFor(values: ComponentValues, locks: ComponentLocks|undefined): ComponentActionRow[] { if (!locks) { return [] } const items = RollTableOrder.filter((v) => values.hasOwnProperty(v)) const lockedItems = items.filter((v) => locks[v] === true) const selectOptions: ComponentSelectOption[] = items.map((v) => ({ default: !(locks[v] ?? false), value: `${v}`, label: RollTableNames[v], emoji: {name: RollTableEmoji[v]} })) 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: lockedItems.length === items.length, emoji: {name: '\u{1f3b2}'}, label: (lockedItems.length === 0 ? "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(values: ComponentValues, locks: ComponentLocks|undefined): MessageOptions { return { embeds: [generateEmbedFor(values, locks)], components: generateActionsFor(values, locks), ephemeral: false } } export function generateErrorMessageFor(e: unknown, context?: string): MessageOptions { console.error(`Error when trying to ${context ?? "do something (unknown context)"}`, e) return { content: `I wasn't able to ${context ?? "do that"}. Thing is, ${e}...`, ephemeral: true, } }