Scenario generator for vore roleplay and story ideas. https://scenario-generator.deliciousreya.net/responses
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

208 lines
6.7 KiB

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<string, string>()
const selection = new Set<string>()
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<T extends {error: unknown, context?: string, extraData?: string}>(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
};
}