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.
 
 

452 lines
15 KiB

export interface RollTableLimited {
readonly full: false,
readonly emoji: string,
readonly title: string,
readonly header: string,
readonly ordinal: number,
readonly results?: null,
}
export interface RollTableDetailsBase {
readonly id: number,
readonly identifier: string,
readonly emoji: string,
readonly name: string,
readonly title: string,
readonly header: string,
readonly ordinal: number,
}
export type RollTable = RollTableLimited | RollTableDetails
export type RollTableOrInput = RollTable | RollTableDetailsInput
export function rollTableToString(v: RollTable) {
if (v.full) {
return `${v.header} (${v.id}/${v.identifier}/${v.name}/${v.emoji}/${v.title}/#${v.ordinal})${v.full === 'results' ? ` [${v.results.size} results]` : '' }`
} else {
return `${v.header} (???#${v.ordinal})`
}
}
export function rollTableToStringShort(v: RollTable) {
if (v.full) {
return v.identifier
} else {
return v.header
}
}
export const MAX_RESULT_LENGTH = 150;
export const MAX_IDENTIFIER_LENGTH = 20;
export const MAX_NAME_LENGTH = 50;
export const MAX_URL_LENGTH = 100;
export interface RollTableAuthor {
readonly id: number;
readonly name: string;
readonly url: string | null;
readonly relation: string;
}
export interface RollTableResultSet {
readonly id: number;
readonly name: string | null;
readonly description: string | null;
readonly global: boolean;
}
export interface RollTableResultLimited<T extends RollTableOrInput = RollTable> {
readonly full: false,
readonly text: string,
readonly table: T,
}
export interface RollTableResultFull<T extends RollTableOrInput = RollTableDetails> {
readonly full: true,
readonly textId: number,
readonly mappingId: number,
readonly table: T,
readonly tableId?: never
readonly text: string,
readonly set: RollTableResultSet,
readonly author: RollTableAuthor | null,
readonly updated: Date,
}
export type RollTableResult<T extends RollTableOrInput = RollTable> = RollTableResultLimited<T> | RollTableResultFull<T>
export type RollTableResultOrLookup<T extends RollTableOrInput = RollTable> = RollTableResultFull<T>|RollTableResultLookup
function setToString(v: RollTableResultSet): string {
return `${v.global ? 'global' : 'local'} ${v.name ?? 'set'}`
}
function authorToString(v: RollTableAuthor): string {
return `${v.relation} ${v.name} (${v.id})`
}
function resultToString(v: RollTableResult) {
if (v.full) {
return `${v.text} (${v.mappingId}: ${v.textId}/${rollTableToStringShort(v.table)}/${setToString(v.set)}/${v.author ? authorToString(v.author) : 'no author'})`
} else {
return `${v.text} (???: ${rollTableToStringShort(v.table)})`
}
}
export interface RollTableResultLookup {
readonly textId: number,
readonly mappingId: number,
readonly tableId: number,
readonly table?: never,
readonly text: string,
readonly setId: number,
readonly authorId: number | null,
readonly updated: Date,
}
export interface RollTableDetailsInputResults extends RollTableDetailsBase {
readonly results: Iterable<RollTableResultOrLookup<RollTableDetailsInputResults>|readonly [number, RollTableResultOrLookup<RollTableDetailsInputResults>]>;
}
function isResultArray(v: unknown): v is readonly [unknown, RollTableResultOrLookup<RollTableDetailsOrInput>] {
return Array.isArray(v) && isRollTableResult(v[1])
}
export interface RollTableDetailsInputNoResults extends RollTableDetailsBase {
readonly results?: null
}
export type RollTableDetailsInput = RollTableDetailsInputResults | RollTableDetailsInputNoResults
export type RollTableDetailsOrInput = RollTableDetails | RollTableDetailsInput
export interface RollTableDetailsNoResults extends RollTableDetailsBase {
readonly full: 'details'
readonly results?: null;
}
export interface RollTableDetailsAndResults extends RollTableDetailsBase {
readonly full: 'results'
readonly results: ReadonlyMap<number, RollTableResultFull<this>>;
}
interface RollTableDetailsAndResultsInternal extends RollTableDetailsBase {
readonly full: 'results'
readonly results: Map<number, RollTableResultFull<this>>;
}
export type RollTableDetails = RollTableDetailsNoResults|RollTableDetailsAndResults
function compareRollTables(a: RollTableOrInput, b: RollTableOrInput): number {
return (a.ordinal - b.ordinal) ||
("id" in a !== "id" in b ? "id" in a ? -1 : 1 : 0) ||
("id" in a && "id" in b ? a.id - b.id : 0) ||
(a.header > b.header ? 1 : a.header < b.header ? -1 : 0);
}
function isRollTableResult(result: unknown): result is RollTableResult<RollTableDetailsOrInput> {
return (typeof result === "object" && result !== null && 'table' in result
&& !('tableId' in result && typeof result.tableId !== 'undefined') && 'full' in result);
}
export class RollTableMap<T extends RollTableOrInput> extends Map<T extends RollTable ? number : (number|string), T> {
[Symbol.iterator](): IterableIterator<[T extends RollTable ? number : (number|string), T]> {
return this.entries();
}
set(key: T extends RollTable ? number : (number|string), table: T): this
set(table: T): this
set(keyOrTable: (T extends RollTable ? number : (number|string))|T, table?: T): this {
if (typeof keyOrTable === "object") {
if ("id" in keyOrTable) {
return super.set(keyOrTable.id, keyOrTable)
} else {
return super.set(keyOrTable.header as (T extends RollTable ? number : (number|string)), keyOrTable)
}
} else {
return super.set(keyOrTable, table!)
}
}
entries(): IterableIterator<[T extends RollTable ? number : (number|string), T]> {
return Array.from(super.entries()).sort(([, a], [, b]) => compareRollTables(a, b))[Symbol.iterator]();
}
keys(): IterableIterator<T extends RollTable ? number : (number|string)> {
return Array.from(this.entries()).map(([id]) => id)[Symbol.iterator]();
}
values(): IterableIterator<T> {
return Array.from(this.entries()).map(([, value]) => value)[Symbol.iterator]();
}
}
export class RollTableDatabase implements Iterable<RollTableDetailsAndResults> {
private readonly tablesById: RollTableMap<RollTableDetailsAndResultsInternal> = new RollTableMap<RollTableDetailsAndResultsInternal>();
private readonly setsById: Map<number, RollTableResultSet> =
new Map<number, RollTableResultSet>();
private readonly authorsById: Map<number, RollTableAuthor> =
new Map<number, RollTableAuthor>;
private readonly mappingsByMappingId: Map<number, RollTableResultFull<RollTableDetailsAndResultsInternal>> =
new Map<number, RollTableResultFull<RollTableDetailsAndResultsInternal>>();
private readonly mappingsByTextId: Map<number, RollTableResultFull<RollTableDetailsAndResultsInternal>> =
new Map<number, RollTableResultFull<RollTableDetailsAndResultsInternal>>();
constructor({ tables = [], results = [], authors = [], sets = [] }: {
tables?: Iterable<RollTableDetailsOrInput>,
results?: Iterable<RollTableResultFull<RollTableDetailsInput> | RollTableResultLookup>,
authors?: Iterable<RollTableAuthor>,
sets?: Iterable<RollTableResultSet>
} = {}) {
for (const table of tables) {
this.addTable(table);
}
for (const author of authors) {
this.addAuthor(author);
}
for (const set of sets) {
this.addSet(set);
}
for (const result of results) {
this.addResult(result);
}
}
[Symbol.iterator](): IterableIterator<RollTableDetailsAndResults> {
return this.tablesById.values();
}
get tables(): ReadonlyMap<number | string, RollTableDetailsAndResults> {
return this.tablesById;
}
get sets(): ReadonlyMap<number, RollTableResultSet> {
return this.setsById;
}
get authors(): ReadonlyMap<number, RollTableAuthor> {
return this.authorsById;
}
get mappings(): ReadonlyMap<number, RollTableResultFull<RollTableDetailsAndResults>> {
return this.mappingsByMappingId;
}
get results(): ReadonlyMap<number, RollTableResultFull<RollTableDetailsAndResults>> {
return this.mappingsByTextId;
}
addTable(table: RollTableDetailsInput): RollTableDetailsAndResults {
return this.addTableInternal(table);
}
private addTableInternal(table: RollTableDetailsInput): RollTableDetailsAndResultsInternal {
const existingTable = this.tablesById.get(table.id);
if (existingTable) {
if (table.results) {
for (const result of table.results) {
this.addResult(result);
}
}
return existingTable;
}
const internalTable: RollTableDetailsAndResultsInternal = {
...table,
full: 'results',
results: new Map<number, RollTableResultFull<RollTableDetailsAndResultsInternal>>()
};
if (table.results) {
for (const result of table.results) {
this.addResult(result);
}
}
this.tablesById.set(table.id, internalTable);
return internalTable;
}
addAuthor(author: RollTableAuthor): RollTableAuthor {
const existingAuthor = this.authorsById.get(author.id);
if (existingAuthor) {
return existingAuthor;
} else {
const result = { ...author };
this.authorsById.set(author.id, author);
return result;
}
}
addSet(set: RollTableResultSet): RollTableResultSet {
const existingSet = this.setsById.get(set.id);
if (existingSet) {
return existingSet;
} else {
const result = { ...set };
this.setsById.set(set.id, set);
return result;
}
}
addResult(result: RollTableResultOrLookup<RollTableDetailsOrInput>|readonly [number, RollTableResultOrLookup<RollTableDetailsOrInput>]): RollTableResultFull {
if (isResultArray(result)) {
const [, innerResult] = result as [number, RollTableResultOrLookup<RollTableDetailsOrInput>];
return this.addResult(innerResult);
} else if (isRollTableResult(result)) {
const internalTable =
this.tablesById.get(result.table.id) ?? this.addTableInternal({... result.table, results: null});
const internalAuthor =
result.full && result.author ? (this.authorsById.get(result.author.id) ?? this.addAuthor(result.author)) : null;
const internalSet = this.setsById.get(result.set.id) ?? this.addSet(result.set);
const out: RollTableResultFull<RollTableDetailsAndResultsInternal> = {
...result,
table: internalTable,
author: internalAuthor,
set: internalSet
};
internalTable.results.set(out.textId, out);
this.mappingsByTextId.set(out.textId, out);
this.mappingsByMappingId.set(out.mappingId, out);
return out;
} else {
const internalTable = this.tablesById.get(result.tableId);
const internalAuthor = typeof result.authorId === 'number' ? this.authorsById.get(result.authorId) : null;
const internalSet = this.setsById.get(result.setId);
if (typeof internalTable === 'undefined') {
throw Error(`no known table with ID ${result.tableId}`);
} else if (typeof internalAuthor === 'undefined') {
throw Error(`no known author with ID ${result.authorId}`);
} else if (typeof internalSet === 'undefined') {
throw Error(`no known set with ID ${result.setId}`);
}
const out: RollTableResultFull<RollTableDetailsAndResultsInternal> = {
full: true,
textId: result.textId,
mappingId: result.mappingId,
text: result.text,
table: internalTable,
author: internalAuthor,
set: internalSet,
updated: result.updated
};
internalTable.results.set(out.textId, out);
this.mappingsByTextId.set(out.textId, out);
this.mappingsByMappingId.set(out.mappingId, out);
return out;
}
}
}
function rollOn(table: RollTableDetailsAndResults): RollTableResult<RollTableDetailsAndResults> {
const results = Array.from(table.results.values());
if (results.length === 0) {
throw Error(`no results for table ${table.identifier}`);
}
return results[Math.floor(results.length * Math.random())];
}
function rollOnAll(tables: Iterable<RollTableDetailsAndResults>): RolledValues<RollTableDetailsAndResults> {
const result = new RolledValues<RollTableDetailsAndResults>();
for (const table of tables) {
result.set(table, rollOn(table));
}
return result;
}
function rerollOn<T extends RollTable>(tables: Iterable<RollTableDetailsAndResults>, original: Iterable<[T, RollTableResult<T>]>): RolledValues<T|RollTableDetailsAndResults> {
const result = new RolledValues<T|RollTableDetailsAndResults>();
const tableSet = new Set<RollTable>(tables);
for (const [table, originalValue] of original) {
if (tableSet.has(table) && table.full === 'results') {
const newValue = rollOn(table);
result.set(table, newValue);
} else {
result.set(table, originalValue);
}
}
return result;
}
export interface FinalGeneratedState<T extends RollTableOrInput = RollTable> {
readonly final: true,
readonly rolled: ReadonlyMap<T, RollTableResult<T>>
}
export interface InProgressGeneratedState<T extends RollTableOrInput = RollTable> {
readonly final: false,
readonly rolled: ReadonlyMap<T, RollTableResult<T>>
readonly selected: ReadonlySet<T>
}
export function generatedStateToString(contents: GeneratedState): string {
if (contents.final) {
return `Final state: ${Array.from(contents.rolled).map(([key, value]) => `${rollTableToString(key)} : ${resultToString(value)}`).join(" ::: ")}`
} else {
return `Current state: ${Array.from(contents.rolled).map(([key, value]) => `${rollTableToString(key)} : ${resultToString(value)}`).join(" ::: ")}. Selection: ${Array.from(contents.selected).map(v => `${rollTableToStringShort(v)}`).join(", ")}`
}
}
export type GeneratedState = FinalGeneratedState | InProgressGeneratedState
export interface FinalGeneratedContents {
readonly final: true,
readonly rolled: ReadonlyMap<string, string>
}
export interface InProgressGeneratedContents {
readonly final: false,
readonly rolled: ReadonlyMap<string, string>;
readonly selected: ReadonlySet<string>;
}
export type GeneratedContents = FinalGeneratedContents | InProgressGeneratedContents
export function generatedContentsToString(contents: GeneratedContents): string {
if (contents.final) {
return `Final contents: ${Array.from(contents.rolled).map(([key, value]) => `${key} : ${value}`).join(" ::: ")}`
} else {
return `Current contents: ${Array.from(contents.rolled).map(([key, value]) => `${key} : ${value}`).join(" ::: ")}. Selection: ${Array.from(contents.selected).join(", ")}`
}
}
export class RolledValues<T extends RollTable = RollTable, U extends RollTableResult<T> = RollTableResult<T>> extends Map<T, U> {
[Symbol.iterator](): IterableIterator<[T, U]> {
return this.entries();
}
add(v: U): this {
return this.set(v.table, v)
}
hasResult(v: U): boolean {
return this.get(v.table) === v
}
entries(): IterableIterator<[T, U]> {
return Array.from(super.entries())
.sort(([a], [b]) =>
compareRollTables(a, b))[Symbol.iterator]();
}
keys(): IterableIterator<T> {
return Array.from(this.entries()).map(([key]) => key)[Symbol.iterator]();
}
values(): IterableIterator<U> {
return Array.from(this.entries()).map(([, value]) => value)[Symbol.iterator]();
}
}
export class RollSelections<T extends RollTable = RollTable> extends Set<T> {
[Symbol.iterator](): IterableIterator<T> {
return this.values();
}
entries(): IterableIterator<[T, T]> {
return Array.from(this.entries()).sort(([a], [b]) => compareRollTables(a, b))[Symbol.iterator]();
}
keys(): IterableIterator<T> {
return Array.from(this.entries()).map(([key]) => key)[Symbol.iterator]();
}
values(): IterableIterator<T> {
return super.values();
}
}