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.
 
 
vore-scenario-generator/src/client/generator-entrypoint.ts

294 lines
9.6 KiB

import {
ExportFormat,
exportScenario,
type GeneratedState,
generatedStateToString,
getResultFrom,
RolledValues,
rollOn,
RollSelections,
type RollTable,
RollTableDatabase,
type RollTableDetailsAndResults,
type RollTableResult,
type RollTableResultFull
} from '../common/rolltable';
import { buildGenerated, htmlTableIdentifier } from '../common/template';
import { DOMLoaded } from './onload';
import { scrapeGeneratedScenario } from './scraper';
import { showPopup } from './popup';
import { pulseElement } from './pulse';
import { DOMTemplateBuilder } from './template';
import escapeHTML from 'escape-html';
export interface RerollEventDetail {
rerolledAll: boolean
changedResults: Iterable<RollTableResultFull<RollTableDetailsAndResults>>
fullResults: ReadonlyMap<RollTable, RollTableResult>
selections: ReadonlySet<RollTable>
}
export class Generator {
readonly generator: HTMLElement;
readonly scenario: HTMLUListElement;
readonly copyButtons: HTMLElement;
readonly rollButtons: HTMLElement;
readonly db: RollTableDatabase | undefined;
private readonly rolled = new RolledValues();
private readonly selected = new RollSelections();
get state(): GeneratedState {
return {
final: false,
rolled: this.rolled,
selected: this.selected,
}
}
getTableWithHtmlId(id: string, prefix?: string): RollTable | undefined {
return Array.from(this.rolled.keys()).find(t => id === ((prefix ?? '') + htmlTableIdentifier(t)));
}
selectAll(): this {
for (const check of this.scenario.querySelectorAll('input[type=checkbox]:not(:checked)') as Iterable<HTMLInputElement>) {
check.checked = true;
check.dispatchEvent<"change">(new Event("change", {cancelable: true, bubbles: true, composed: false}))
const table = this.getTableWithHtmlId(check.id, 'selected-');
if (table) {
this.selected.add(table);
}
}
return this
}
selectNone(): this {
this.selected.clear();
for (const check of this.scenario.querySelectorAll('input[type=checkbox]:checked') as Iterable<HTMLInputElement>) {
check.checked = false;
check.dispatchEvent<"change">(new Event("change", {cancelable: true, bubbles: true, composed: false}))
}
return this
}
reroll(all: boolean): this {
if (!this.db) {
return this
}
const changes: RollTableResultFull<RollTableDetailsAndResults>[] = []
for (const row of this.scenario.querySelectorAll(`.generatedElement`)) {
const check = row.querySelector<HTMLInputElement>(`input.generatedSelect:checked`)
const text = row.querySelector<HTMLElement>(`.resultText`)
if ((all || check) && text) {
let result = this.db.mappings.get(parseInt(text.dataset["mappingid"] ?? '-1'))
if (!result || result.table.resultsById.size === 1) {
continue
}
const origResult = result
const table = result.table
while (result === origResult) {
result = rollOn(table)
}
this.setActiveResult(result, all)
changes.push(result)
pulseElement(text)
}
}
this.generator.dispatchEvent(new CustomEvent<RerollEventDetail>("reroll", {
composed: false,
bubbles: true,
cancelable: false,
detail: {
rerolledAll: all,
changedResults: changes,
fullResults: this.rolled,
selections: this.selected,
}
}))
return this
}
loadValuesFromDOM(): this {
this.rolled.clear()
this.selected.clear()
const scenario = scrapeGeneratedScenario(this.scenario)
if (!scenario) {
throw Error("Failed to load generated values from DOM")
}
for (const [scrapedTable, scrapedResult] of scenario.rolled) {
const table = this.db?.getTableMatching(scrapedTable) ?? scrapedTable
const result = getResultFrom(table, scrapedResult)
if (scenario.selected.has(scrapedTable)) {
this.selected.add(table)
}
this.rolled.add(result)
}
return this
}
private getGeneratedElementForTable(table: RollTable): HTMLElement|null {
return this.scenario.querySelector(`#generated-${escapeHTML(htmlTableIdentifier(table))}`)
}
private replaceResultInElement(result: RollTableResult, generatedElement: HTMLElement) {
// sister function is buildGeneratedElement
const generatedDiv = generatedElement.querySelector(`.generated`)
if (!generatedDiv) {
throw Error(`couldn't find .generated in replaceResultInElement`)
}
generatedDiv.replaceWith(buildGenerated({result, includesResponses: !!this.db, builder: DOMTemplateBuilder}))
const button = generatedElement.querySelector<HTMLButtonElement>(`.resultText`)!
pulseElement(button)
this.rolled.add(result)
}
private changeSelection(generatedElement: HTMLElement, selected: boolean) {
const check = generatedElement.querySelector<HTMLInputElement>(`.generatedSelect`)
if (!check) {
return
}
if (check.checked !== selected) {
check.checked = selected
check.dispatchEvent<"change">(new Event("change", {cancelable: true, bubbles: true, composed: false}))
}
}
async copy(format: ExportFormat): Promise<void> {
const exported = exportScenario(Array.from(this.rolled.values()), format)
return navigator.clipboard.writeText(exported)
}
attachHandlers(): this {
this.generator.addEventListener('click', (e) => this.clickHandler(e));
this.generator.addEventListener('change', (e) => this.changeHandler(e));
this.generator.querySelector<HTMLButtonElement>(`#reroll`)!.disabled = (this.selected.size === 0)
return this;
}
private clickHandler(e: Event): void {
if (e.target instanceof HTMLButtonElement || e.target instanceof HTMLAnchorElement) {
switch (e.target.id) {
case "selectNone":
this.selectNone()
break
case "selectAll":
this.selectAll()
break
case "copyMD":
this.copy(ExportFormat.Markdown)
.then(() => showPopup(this.copyButtons, `Copied Markdown to clipboard!`, 'success'))
.catch((e) => {
console.error("Failed while copying Markdown:", e)
showPopup(this.copyButtons, `Failed to copy Markdown to clipboard`, 'error')
})
break
case "copyBB":
this.copy(ExportFormat.BBCode)
.then(() => showPopup(this.copyButtons, `Copied BBCode to clipboard!`, 'success'))
.catch((e) => {
console.error("Failed while copying BBCode:", e)
showPopup(this.copyButtons, `Failed to copy BBCode to clipboard`, 'error')
})
break
case "copyEmojiText":
this.copy(ExportFormat.TextEmoji)
.then(() => showPopup(this.copyButtons, `Copied text (with emojis) to clipboard!`, 'success'))
.catch((e) => {
console.error("Failed while copying text (with emojis):", e)
showPopup(this.copyButtons, `Failed to copy text (with emojis) to clipboard`, 'error')
})
break
case "copyText":
this.copy(ExportFormat.TextOnly)
.then(() => showPopup(this.copyButtons, `Copied text to clipboard!`, 'success'))
.catch((e) => {
console.error("Failed while copying text:", e)
showPopup(this.copyButtons, `Failed to copy text to clipboard`, 'error')
})
break
case "reroll":
this.reroll(false)
break
case "rerollAll":
this.reroll(true)
break
default:
return
}
e.preventDefault()
}
}
private changeHandler(e: Event): void {
if (e.target instanceof HTMLInputElement && e.target.type === 'checkbox' && e.target.id.startsWith('selected-')) {
const check = e.target
const table = this.getTableWithHtmlId(check.id, 'selected-');
if (table) {
if (check.checked) {
this.selected.add(table);
} else {
this.selected.delete(table);
}
this.generator.querySelector<HTMLButtonElement>(`#reroll`)!.disabled = (this.selected.size === 0)
pulseElement(check)
}
}
}
constructor(generator: HTMLElement, generatorForm: HTMLUListElement, copyButtons: HTMLElement, rollButtons: HTMLElement, db?: RollTableDatabase) {
this.generator = generator;
this.scenario = generatorForm;
this.copyButtons = copyButtons;
this.rollButtons = rollButtons;
this.db = db;
}
setActiveResult(result: RollTableResultFull<RollTableDetailsAndResults>, clearSelection?: boolean) {
const tableElement = this.getGeneratedElementForTable(result.table)
if (!tableElement) {
return
}
this.replaceResultInElement(result, tableElement)
if (clearSelection) {
this.changeSelection(tableElement, false)
}
}
addRerollListener(listener: (ev: CustomEvent<RerollEventDetail>) => void, options?: boolean|EventListenerOptions): void {
this.generator.addEventListener('reroll', listener, options)
}
}
function initGenerator(db?: RollTableDatabase): Generator {
const generatorFound = document.getElementById('generator');
if (!generatorFound) {
throw Error('generator was not found');
}
const generatedScenarioFound = document.getElementById('generatedScenario');
if (!generatedScenarioFound || !(generatedScenarioFound instanceof HTMLUListElement)) {
throw Error('generated scenario was not found');
}
const copyButtons = document.getElementById("copyButtons")
if (!copyButtons) {
throw Error('copy buttons were not found')
}
const rollButtons = document.getElementById("rollButtons")
if (!rollButtons) {
throw Error('roll buttons were not found')
}
return new Generator(generatorFound, generatedScenarioFound, copyButtons, rollButtons, db).loadValuesFromDOM().attachHandlers();
}
let pendingGenerator: Promise<Generator>|undefined = undefined
export async function prepareGenerator(db?: Promise<RollTableDatabase>): Promise<Generator> {
if (pendingGenerator) {
throw Error(`prepareGenerator should only be called once`)
}
pendingGenerator = DOMLoaded.then(() => db)
.then((promisedDb) => initGenerator(promisedDb))
return pendingGenerator
}
DOMLoaded.then(() => pendingGenerator ?? prepareGenerator())
.then(g => console.info(`loaded generator:\n${generatedStateToString(g.state)}`))
.catch(e => console.error('failed to load generator', e))