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.
294 lines
9.6 KiB
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))
|
|
|