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