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.
380 lines
12 KiB
380 lines
12 KiB
import {
|
|
type FinalGeneratedContents,
|
|
type FinalGeneratedState,
|
|
type GeneratedContents,
|
|
type GeneratedState,
|
|
type InProgressGeneratedContents,
|
|
type InProgressGeneratedState,
|
|
RolledValues, rollOn, rollResultToString,
|
|
RollSelections,
|
|
type RollTable,
|
|
type RollTableAuthor,
|
|
RollTableDatabase,
|
|
type RollTableDetailsNoResults,
|
|
type RollTableResultFull,
|
|
} from '../../common/rolltable';
|
|
import { type PreparedQueries, type QueryOutput, TypedDBWrapper } from './querytypes';
|
|
import { DatabaseQueries } from './queries';
|
|
|
|
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
|
|
};
|
|
}
|
|
|
|
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'
|
|
};
|
|
}
|
|
|
|
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 getGeneratorDataForDiscordSet(reroll: true, setSnowflake: string | null, oldResults?: InProgressGeneratedContents | null, finalize?: false): Promise<InProgressGeneratedState & {
|
|
db: RollTableDatabase
|
|
}>
|
|
private async getGeneratorDataForDiscordSet(reroll: false, setSnowflake: string | null, oldResults?: null): Promise<RollTableDatabase>
|
|
private async getGeneratorDataForDiscordSet(reroll: false, setSnowflake: string | null, oldResults: GeneratedContents, finalize: false): Promise<InProgressGeneratedState & {
|
|
db: RollTableDatabase
|
|
}>
|
|
private async getGeneratorDataForDiscordSet(reroll: false, setSnowflake: string | null, oldResults: GeneratedContents, finalize: true): Promise<FinalGeneratedState & {
|
|
db: RollTableDatabase
|
|
}>
|
|
private async getGeneratorDataForDiscordSet(reroll: false, setSnowflake: string | null, oldResults: InProgressGeneratedContents): Promise<InProgressGeneratedState & {
|
|
db: RollTableDatabase
|
|
}>
|
|
private async getGeneratorDataForDiscordSet(reroll: false, setSnowflake: string | null, oldResults: FinalGeneratedContents): Promise<FinalGeneratedState & {
|
|
db: RollTableDatabase
|
|
}>
|
|
private async getGeneratorDataForDiscordSet(reroll: boolean, setSnowflake: string | null, oldResults: GeneratedContents, finalize?: boolean): Promise<GeneratedState & {
|
|
db: RollTableDatabase
|
|
}>
|
|
private async getGeneratorDataForDiscordSet(reroll: boolean, setSnowflake: string | null, oldResults?: GeneratedContents | null, finalize?: boolean): Promise<RollTableDatabase | (GeneratedState & {
|
|
db: RollTableDatabase
|
|
})> {
|
|
const oldHeaders = oldResults && oldResults.rolled.size > 0 ? Array.from(oldResults.rolled.keys()) : [];
|
|
const [tables, oldKeys, oldSelection, { mappings, sets, authors }] =
|
|
await this.db.batch(
|
|
this.queries.getTables({}),
|
|
this.queries.getTableIdsByIdentifierOrHeader({
|
|
identifiersOrHeaders: oldHeaders
|
|
}),
|
|
this.queries.getTableIdsByIdentifierOrHeader({
|
|
identifiersOrHeaders: oldResults && !oldResults.final && oldResults.selected.size > 0 ? Array.from(oldResults.selected) : []
|
|
}),
|
|
this.queries.getFullDatabaseForDiscordSet({
|
|
setSnowflake
|
|
}));
|
|
const db = new RollTableDatabase({
|
|
tables: tables.map(v => ({ ...v, full: 'details' })),
|
|
authors,
|
|
sets,
|
|
results: mappings.map(v => ({ ...v, updated: new Date(v.updated) }))
|
|
});
|
|
if (!oldResults && !reroll) {
|
|
return db;
|
|
}
|
|
const selected = new RollSelections(oldSelection.flatMap(v => {
|
|
if (v === null) {
|
|
return [];
|
|
}
|
|
const table = db.tables.get(v);
|
|
if (!table) {
|
|
return [];
|
|
}
|
|
return [table];
|
|
}));
|
|
const rolled = new RolledValues();
|
|
const rollKeys = oldResults ? oldKeys : tables.map(t => t.id);
|
|
for (let index = 0; index < rollKeys.length; index += 1) {
|
|
const tableId = rollKeys[index];
|
|
const lookupTable = tableId !== null ? db.tables.get(tableId) : null;
|
|
const oldHeader = oldHeaders[index];
|
|
const [oldEmoji, oldTitle] = oldHeader ? oldHeader.split(' ', 2) : ['', ''];
|
|
const table: RollTable = lookupTable ?? {
|
|
full: false,
|
|
header: oldHeader,
|
|
emoji: oldEmoji,
|
|
title: oldTitle,
|
|
ordinal: index
|
|
};
|
|
const text = oldResults?.rolled.get(oldHeader);
|
|
if (reroll && table.full && (!text || selected.has(table))) {
|
|
const result = rollOn(table)
|
|
rolled.add(result);
|
|
} else if (text) {
|
|
const lookupResult = text && table.full === 'results' ? table.resultsByText.get(text) : null;
|
|
const result = lookupResult ?? {
|
|
full: false,
|
|
text: text,
|
|
table
|
|
}
|
|
rolled.add(result);
|
|
}
|
|
}
|
|
return (finalize ?? oldResults?.final) ? {
|
|
final: true,
|
|
db,
|
|
rolled
|
|
} : {
|
|
final: false,
|
|
db,
|
|
rolled,
|
|
selected
|
|
};
|
|
}
|
|
|
|
async getGeneratorPageForDiscordSet(setSnowflake: string | null, oldResults?: InProgressGeneratedContents | null): Promise<InProgressGeneratedState & {
|
|
db: RollTableDatabase
|
|
}> {
|
|
return this.getGeneratorDataForDiscordSet(true, setSnowflake, oldResults, 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.getGeneratorDataForDiscordSet(false, setId, contents);
|
|
}
|
|
|
|
async generateFromDiscordSet(setId: string): Promise<InProgressGeneratedState> {
|
|
return this.getGeneratorDataForDiscordSet(true, setId);
|
|
}
|
|
|
|
async rerollFromDiscordSet(setId: string, existing: InProgressGeneratedContents): Promise<InProgressGeneratedState> {
|
|
return this.getGeneratorDataForDiscordSet(true, setId, existing);
|
|
}
|
|
|
|
async reopenFromDiscordSet(setId: string, existing: FinalGeneratedContents): Promise<InProgressGeneratedState> {
|
|
return this.getGeneratorDataForDiscordSet(false, setId, existing, false);
|
|
}
|
|
|
|
async finalizeFromDiscordSet(setId: string, existing: InProgressGeneratedContents): Promise<FinalGeneratedState> {
|
|
return this.getGeneratorDataForDiscordSet(false, setId, existing, true);
|
|
}
|
|
}
|
|
|