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.
473 lines
15 KiB
473 lines
15 KiB
import {
|
|
type FinalGeneratedContents,
|
|
type FinalGeneratedState,
|
|
type GeneratedContents,
|
|
type GeneratedState,
|
|
type InProgressGeneratedContents,
|
|
type InProgressGeneratedState, RolledValues, RollSelections,
|
|
type RollTable,
|
|
type RollTableAuthor, RollTableDatabase,
|
|
type RollTableDetailsNoResults,
|
|
type RollTableResult,
|
|
type RollTableResultFull
|
|
} from '../../common/rolltable.js';
|
|
import { type PreparedQueries, type QueryOutput, TypedDBWrapper } from './querytypes.js';
|
|
import {
|
|
DatabaseQueries,
|
|
} from './queries.js';
|
|
import { recordError } from '../discord/embed.js';
|
|
|
|
function processOperationResult(result: QueryOutput<(typeof DatabaseQueries)["getResultMappingsForDiscordSet"]>[number] | undefined): (RollTableResultFull<RollTableDetailsNoResults> & {
|
|
status: 'updated' | 'existing'
|
|
}) | undefined {
|
|
if (!result) {
|
|
return result;
|
|
}
|
|
return {
|
|
full: true,
|
|
mappingId: result.mappingId,
|
|
textId: result.resultId,
|
|
text: result.resultText,
|
|
table: {
|
|
full: 'details',
|
|
id: result.tableId,
|
|
identifier: result.tableIdentifier,
|
|
name: result.tableName,
|
|
title: result.tableTitle,
|
|
emoji: result.tableEmoji,
|
|
header: result.tableHeader,
|
|
ordinal: result.tableOrdinal
|
|
},
|
|
author: (result.authorId === null || result.authorName === null || result.authorRelation === null) ? null : {
|
|
id: result.authorId,
|
|
name: result.authorName,
|
|
url: result.authorUrl,
|
|
relation: result.authorRelation
|
|
},
|
|
set: {
|
|
id: result.setId,
|
|
name: result.setName,
|
|
description: result.setDescription,
|
|
global: !!(result.setGlobal)
|
|
},
|
|
updated: new Date(result.updated),
|
|
status: result.status
|
|
};
|
|
}
|
|
|
|
function processGeneratedRow(result: QueryOutput<typeof DatabaseQueries['generateFromDiscord']>[number]): RollTableResult & { selected: boolean } {
|
|
if (result.tableId === null) {
|
|
return {
|
|
full: false,
|
|
table: {
|
|
full: false,
|
|
emoji: result.tableEmoji,
|
|
title: result.tableTitle,
|
|
header: result.tableHeader,
|
|
ordinal: result.tableOrdinal
|
|
},
|
|
text: result.resultText,
|
|
selected: false
|
|
};
|
|
} else if (result.mappingId === null) {
|
|
return {
|
|
full: false,
|
|
table: {
|
|
full: 'details',
|
|
emoji: result.tableEmoji,
|
|
header: result.tableHeader,
|
|
id: result.tableId,
|
|
identifier: result.tableIdentifier,
|
|
name: result.tableName,
|
|
ordinal: result.tableOrdinal,
|
|
title: result.tableTitle
|
|
},
|
|
text: result.resultText,
|
|
selected: result.selected
|
|
};
|
|
} else {
|
|
return {
|
|
full: true,
|
|
table: {
|
|
full: 'details',
|
|
emoji: result.tableEmoji,
|
|
header: result.tableHeader,
|
|
id: result.tableId,
|
|
identifier: result.tableIdentifier,
|
|
name: result.tableName,
|
|
ordinal: result.tableOrdinal,
|
|
title: result.tableTitle
|
|
},
|
|
author: result.authorId && result.authorName && result.authorRelation ? {
|
|
id: result.authorId,
|
|
name: result.authorName,
|
|
url: result.authorUrl,
|
|
relation: result.authorRelation
|
|
} : null,
|
|
set: {
|
|
id: result.setId,
|
|
name: result.setName,
|
|
description: result.setDescription,
|
|
global: !!(result.setGlobal)
|
|
},
|
|
mappingId: result.mappingId,
|
|
textId: result.resultId,
|
|
text: result.resultText,
|
|
updated: new Date(result.updated),
|
|
selected: result.selected
|
|
};
|
|
}
|
|
}
|
|
|
|
function processGeneration(results: QueryOutput<typeof DatabaseQueries['generateFromDiscord']>, final: true): FinalGeneratedState
|
|
function processGeneration(results: QueryOutput<typeof DatabaseQueries['generateFromDiscord']>, final: false): InProgressGeneratedState
|
|
function processGeneration(results: QueryOutput<typeof DatabaseQueries['generateFromDiscord']>, final: boolean): GeneratedState
|
|
function processGeneration(results: QueryOutput<typeof DatabaseQueries['generateFromDiscord']>, final: boolean): GeneratedState {
|
|
const rolled = new Map<RollTable, RollTableResult>();
|
|
const selected = new Set<RollTable>();
|
|
for (const rawResult of results) {
|
|
const processed = processGeneratedRow(rawResult);
|
|
rolled.set(processed.table, processed);
|
|
if (!final && processed.selected) {
|
|
selected.add(processed.table);
|
|
}
|
|
}
|
|
return final ? {
|
|
final,
|
|
rolled
|
|
} : {
|
|
final,
|
|
rolled,
|
|
selected
|
|
};
|
|
}
|
|
|
|
export class Database {
|
|
private readonly db: TypedDBWrapper;
|
|
private readonly queries: PreparedQueries<typeof DatabaseQueries>;
|
|
|
|
constructor(db: D1Database) {
|
|
this.db = new TypedDBWrapper(db);
|
|
this.queries = this.db.prepareAll(DatabaseQueries);
|
|
}
|
|
|
|
async autocompleteTable(tableSoFar: string) {
|
|
return this.db.run(this.queries.autocompleteTable({
|
|
tableIdentifierSubstring: tableSoFar
|
|
}));
|
|
}
|
|
|
|
async autocompleteText(setSnowflake: string, tableIdentifier: string, partialText: string, includeGlobal: boolean) {
|
|
return this.db.run(this.queries.autocompleteTextForDiscordSet({
|
|
setSnowflake: setSnowflake,
|
|
tableIdentifierSubstring: tableIdentifier,
|
|
pattern: partialText,
|
|
includeGlobal,
|
|
}));
|
|
}
|
|
|
|
async addResponseFromDiscord(timestamp: number, table: string | number, text: string, userId: string, username: string, setId: string) {
|
|
const [, , , , results] = await this.db.batch(
|
|
this.queries.addResultForAddMapping({ tableIdentifier: table, text }),
|
|
this.queries.addDiscordAuthorForAddMapping({ userSnowflake: userId, username }),
|
|
this.queries.addDiscordSetForAddMapping({ setSnowflake: setId, userSnowflake: userId }),
|
|
this.queries.addDiscordResultMapping({
|
|
timestamp,
|
|
tableIdentifier: table,
|
|
resultText: text,
|
|
userSnowflake: userId,
|
|
setSnowflake: setId
|
|
}),
|
|
this.queries.getResultMappingsForDiscordSet({
|
|
timestamp,
|
|
tableIdentifier: table,
|
|
text,
|
|
setSnowflake: setId,
|
|
includeGlobal: false
|
|
})
|
|
);
|
|
const result = processOperationResult(results[0]);
|
|
if (!result) {
|
|
throw Error('failed adding the new response');
|
|
}
|
|
return {
|
|
...result,
|
|
status: result.status === 'updated' ? 'added' : 'existed'
|
|
};
|
|
}
|
|
|
|
async editResponseFromDiscord(timestamp: number, table: number | string, oldText: string, newText: string, userId: string, username: string, setId: string): Promise<{
|
|
status: 'nonexistent'
|
|
} | {
|
|
status: 'noneditable',
|
|
old: RollTableResultFull<RollTableDetailsNoResults>
|
|
} | {
|
|
status: 'conflict' | 'updated',
|
|
old: RollTableResultFull<RollTableDetailsNoResults>,
|
|
new: RollTableResultFull<RollTableDetailsNoResults>,
|
|
}> {
|
|
const [oldResults, , , , newResults] = await this.db.batch(
|
|
this.queries.getResultMappingsForDiscordSet({
|
|
timestamp,
|
|
tableIdentifier: table,
|
|
text: oldText,
|
|
setSnowflake: setId,
|
|
includeGlobal: true
|
|
}),
|
|
this.queries.addResultForEditMapping({
|
|
tableIdentifier: table,
|
|
oldText,
|
|
newText,
|
|
setSnowflake: setId
|
|
}),
|
|
this.queries.addDiscordAuthorForEditMapping({
|
|
userSnowflake: userId,
|
|
username,
|
|
tableIdentifier: table,
|
|
oldText,
|
|
newText,
|
|
setSnowflake: setId
|
|
}),
|
|
this.queries.editMappingForDiscord({
|
|
timestamp,
|
|
tableIdentifier: table,
|
|
oldText,
|
|
newText,
|
|
userSnowflake: userId,
|
|
setSnowflake: setId
|
|
}),
|
|
this.queries.getResultMappingsForDiscordSet({
|
|
timestamp,
|
|
tableIdentifier: table,
|
|
text: newText,
|
|
setSnowflake: setId,
|
|
includeGlobal: false
|
|
})
|
|
);
|
|
const oldResult = processOperationResult(oldResults[0]);
|
|
if (!oldResult) {
|
|
return { status: 'nonexistent' };
|
|
}
|
|
if (oldResult.set?.global) {
|
|
return { status: 'noneditable', old: oldResult };
|
|
}
|
|
const newResult = processOperationResult(newResults[0]);
|
|
if (!newResult) {
|
|
throw Error('failed to update response');
|
|
}
|
|
return {
|
|
status: newResult.status === 'updated' ? 'updated' : 'conflict',
|
|
old: oldResult,
|
|
new: newResult
|
|
};
|
|
}
|
|
|
|
async deleteResponseFromDiscord(table: number | string, text: string, setId: string): Promise<{
|
|
status: 'nonexistent'
|
|
} | {
|
|
status: 'noneditable' | 'deleted',
|
|
old: RollTableResultFull<RollTableDetailsNoResults>
|
|
}> {
|
|
const [oldResults, deleted] = await this.db.batch(
|
|
this.queries.getResultMappingsForDiscordSet({
|
|
timestamp: null,
|
|
tableIdentifier: table,
|
|
text,
|
|
setSnowflake: setId,
|
|
includeGlobal: true
|
|
}),
|
|
this.queries.deleteDiscordResultMapping({
|
|
tableIdentifier: table,
|
|
text,
|
|
setSnowflake: setId
|
|
})
|
|
);
|
|
const oldResult = processOperationResult(oldResults[0]);
|
|
if (!oldResult) {
|
|
return {
|
|
status: 'nonexistent'
|
|
};
|
|
}
|
|
if (!deleted) {
|
|
return {
|
|
status: 'noneditable',
|
|
old: oldResult
|
|
};
|
|
}
|
|
return {
|
|
status: 'deleted',
|
|
old: oldResult
|
|
};
|
|
}
|
|
|
|
async getResponseFromDiscord(table: number | string, text: string, setId: string): Promise<{
|
|
status: 'nonexistent'
|
|
} | ({
|
|
status: 'existent',
|
|
} & RollTableResultFull<RollTableDetailsNoResults>)> {
|
|
const results = await this.db.run(this.queries.getResultMappingsForDiscordSet({
|
|
timestamp: null,
|
|
tableIdentifier: table,
|
|
text,
|
|
setSnowflake: setId,
|
|
includeGlobal: true,
|
|
}))
|
|
const result = processOperationResult(results[0]);
|
|
if (!result) {
|
|
return {
|
|
status: 'nonexistent'
|
|
};
|
|
}
|
|
return {
|
|
...result,
|
|
status: 'existent'
|
|
};
|
|
}
|
|
|
|
private async runGenerateFromDiscord(reroll: true, setId: string|null, contents?: InProgressGeneratedContents | null, finalize?: false): Promise<InProgressGeneratedState>
|
|
private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: GeneratedContents, finalize: false): Promise<InProgressGeneratedState>
|
|
private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: GeneratedContents, finalize: true): Promise<FinalGeneratedState>
|
|
private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: InProgressGeneratedContents): Promise<InProgressGeneratedState>
|
|
private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: FinalGeneratedContents): Promise<FinalGeneratedState>
|
|
private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: GeneratedContents, finalize?: boolean): Promise<GeneratedState>
|
|
private async runGenerateFromDiscord(reroll: boolean, setId: string|null, contents?: GeneratedContents | null, finalize?: boolean): Promise<GeneratedState> {
|
|
const results = await this.db.run(this.queries.generateFromDiscord({
|
|
reroll,
|
|
setSnowflake: setId,
|
|
original: contents ? Array.from(contents.rolled) : null,
|
|
selection: contents && !contents.final ? Array.from(contents.selected) : null
|
|
}))
|
|
return processGeneration(results, finalize ?? contents?.final ?? false);
|
|
}
|
|
|
|
async expandFromDiscordSet(setId: string, contents: FinalGeneratedContents): Promise<FinalGeneratedState>
|
|
async expandFromDiscordSet(setId: string, contents: InProgressGeneratedContents): Promise<InProgressGeneratedState>
|
|
async expandFromDiscordSet(setId: string, contents: GeneratedContents): Promise<GeneratedState>
|
|
async expandFromDiscordSet(setId: string, contents: GeneratedContents): Promise<GeneratedState> {
|
|
return this.runGenerateFromDiscord(false, setId, contents);
|
|
}
|
|
|
|
async generateFromDiscordSet(setId: string): Promise<InProgressGeneratedState> {
|
|
return this.runGenerateFromDiscord(true, setId);
|
|
}
|
|
|
|
async rerollFromDiscordSet(setId: string, existing: InProgressGeneratedContents): Promise<InProgressGeneratedState> {
|
|
return this.runGenerateFromDiscord(true, setId, existing);
|
|
}
|
|
|
|
async reopenFromDiscordSet(setId: string, existing: FinalGeneratedContents): Promise<InProgressGeneratedState> {
|
|
return this.runGenerateFromDiscord(false, setId, existing, false);
|
|
}
|
|
|
|
async finalizeFromDiscordSet(setId: string, existing: InProgressGeneratedContents): Promise<FinalGeneratedState> {
|
|
return this.runGenerateFromDiscord(false, setId, existing, true);
|
|
}
|
|
|
|
async getDiscordAuthor(id: string): Promise<RollTableAuthor | null> {
|
|
return await this.db.run(this.queries.getDiscordAuthor({
|
|
userSnowflake: id,
|
|
}))
|
|
}
|
|
|
|
async setDiscordAuthor(id: string, username: string, name: string | null, url: string | null): Promise<RollTableAuthor|null> {
|
|
const [, result] = await this.db.batch(
|
|
this.queries.setDiscordAuthor({
|
|
userSnowflake: id,
|
|
username: username,
|
|
name: name,
|
|
url: url,
|
|
}),
|
|
this.queries.getDiscordAuthor({userSnowflake: id})
|
|
)
|
|
return result;
|
|
}
|
|
|
|
private async getWebPageDataForDiscordSet(reroll: true, setSnowflake: string|null, oldResults?: InProgressGeneratedContents | null, finalize?: false): Promise<InProgressGeneratedState & {db: RollTableDatabase}>
|
|
private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults?: null): Promise<RollTableDatabase>
|
|
private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: GeneratedContents, finalize: false): Promise<InProgressGeneratedState & {db: RollTableDatabase}>
|
|
private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: GeneratedContents, finalize: true): Promise<FinalGeneratedState & {db: RollTableDatabase}>
|
|
private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: InProgressGeneratedContents): Promise<InProgressGeneratedState & {db: RollTableDatabase}>
|
|
private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: FinalGeneratedContents): Promise<FinalGeneratedState & {db: RollTableDatabase}>
|
|
private async getWebPageDataForDiscordSet(reroll: boolean, setSnowflake: string|null, oldResults?: GeneratedContents|null, finalize?: boolean): Promise<RollTableDatabase | (GeneratedState & {db: RollTableDatabase})>
|
|
private async getWebPageDataForDiscordSet(reroll: boolean, setSnowflake: string|null, oldResults?: GeneratedContents|null, finalize?: boolean): Promise<RollTableDatabase | (GeneratedState & {db: RollTableDatabase})> {
|
|
const { tables, mappings, results, sets, authors } =
|
|
await this.db.run(this.queries.getFullDatabaseForDiscordSet({
|
|
reroll,
|
|
setSnowflake,
|
|
original: oldResults ? Array.from(oldResults.rolled) : null,
|
|
selection: oldResults && !oldResults.final ? Array.from(oldResults.selected) : null,
|
|
}))
|
|
const db = new RollTableDatabase({
|
|
tables, authors, sets, results: mappings.map(v => ({...v, updated: new Date(v.updated)}))})
|
|
if (!results) {
|
|
return db
|
|
}
|
|
const rolled = new RolledValues()
|
|
for (const result of results) {
|
|
switch (result.type) {
|
|
case 'mapping':
|
|
const mapping = db.mappings.get(result.mappingId)
|
|
if (mapping) {
|
|
rolled.add(mapping)
|
|
} else {
|
|
recordError({
|
|
error: Error(`no mapping with ID ${result.mappingId}`),
|
|
context: 'getting web page data for discord set',
|
|
})
|
|
}
|
|
break
|
|
case 'unknownText':
|
|
const table = db.tables.get(result.tableId)
|
|
if (table) {
|
|
rolled.add({
|
|
full: false,
|
|
text: result.text,
|
|
table
|
|
})
|
|
} else {
|
|
recordError({
|
|
error: Error(`no table with ID ${result.tableId}`),
|
|
context: `assembling unknown text for discord set`
|
|
})
|
|
}
|
|
break
|
|
case 'unknownTable':
|
|
rolled.add({
|
|
full: false,
|
|
table: {
|
|
full: false,
|
|
ordinal: result.ordinal,
|
|
header: result.header,
|
|
emoji: result.emoji,
|
|
title: result.title
|
|
},
|
|
text: result.text
|
|
})
|
|
break
|
|
}
|
|
}
|
|
if (finalize === true || (finalize === null && oldResults?.final)) {
|
|
return {
|
|
final: true,
|
|
db,
|
|
rolled
|
|
}
|
|
}
|
|
const selected = new RollSelections()
|
|
for (const table of tables) {
|
|
if (table.selected) {
|
|
selected.add(db.tables.get(table.id)!)
|
|
}
|
|
}
|
|
return {
|
|
final: false,
|
|
db,
|
|
rolled,
|
|
selected
|
|
}
|
|
}
|
|
|
|
async getGeneratorPageForDiscordSet(setSnowflake: string|null, oldResults?: InProgressGeneratedContents|null): Promise<InProgressGeneratedState & {db: RollTableDatabase}> {
|
|
return this.getWebPageDataForDiscordSet(true, setSnowflake, oldResults, false)
|
|
}
|
|
}
|
|
|