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.
 
 

233 lines
7.9 KiB

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,
}
}