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.
172 lines
7.5 KiB
172 lines
7.5 KiB
|
|
import { FormButton, LinkButton } from './Button';
|
|
import { createContext, Fragment } from 'preact';
|
|
import { usePopup } from './usePopup';
|
|
import { useCallback, useContext } from 'preact/hooks';
|
|
import { ExportFormat, exportFormatToString } from '../rolltable';
|
|
import {
|
|
GeneratedElement,
|
|
type GeneratedElementProps,
|
|
reconstituteGeneratedElement,
|
|
} from './GeneratedElement';
|
|
import { IncludesResponses } from './ResponsesPage';
|
|
import { tableIdentifier } from './TableHeader';
|
|
|
|
export const IncludesGenerator = createContext(false)
|
|
|
|
export interface GeneratorProps {
|
|
generatorTargetUrl: string
|
|
elements: GeneratedElementProps[]
|
|
addToDiscordUrl: string|null
|
|
editable: boolean
|
|
}
|
|
|
|
export enum GeneratorSelect {
|
|
All = "all",
|
|
None = "none",
|
|
}
|
|
|
|
export enum GeneratorReroll {
|
|
All = "all",
|
|
Selected = "selected",
|
|
}
|
|
|
|
export function reconstituteGenerator(element: HTMLDivElement, partial?: Partial<GeneratorProps>): GeneratorProps {
|
|
const addToDiscordUrl = partial?.addToDiscordUrl ?? element.querySelector<HTMLAnchorElement>("#addToDiscord")?.href ?? null
|
|
const editable = partial?.editable ?? !!element.querySelector("#rollButtons")
|
|
const elements = partial?.elements ??
|
|
Array.from(element.querySelector<HTMLUListElement>("#generatedScenario")!.children)
|
|
.map(li => reconstituteGeneratedElement(li as HTMLLIElement, editable ? {} : {selected: null}))
|
|
|
|
const generatorTargetUrl = partial?.generatorTargetUrl ?? element.querySelector<HTMLFormElement>("#generatorWindow")!.action
|
|
return {
|
|
addToDiscordUrl,
|
|
editable,
|
|
generatorTargetUrl,
|
|
elements: elements,
|
|
}
|
|
}
|
|
|
|
export interface GeneratorEvents {
|
|
onCopy?: (format: ExportFormat) => Promise<void>
|
|
onReroll?: (which: GeneratorReroll) => Promise<void>
|
|
onSelect?: (which: GeneratorSelect) => void
|
|
onSelectionChange?: (tableId: number, selected: boolean) => void
|
|
}
|
|
|
|
enum GeneratorSelectionState {
|
|
All = "All",
|
|
Partial = "Some",
|
|
None = "None"
|
|
}
|
|
|
|
export function GeneratorPage({ editable, generatorTargetUrl, addToDiscordUrl, onSelectionChange, onSelect, onReroll, onCopy, elements }: GeneratorProps & GeneratorEvents) {
|
|
const includesResponses = useContext(IncludesResponses);
|
|
const [copyPopupHost, showCopyPopup] = usePopup<HTMLDivElement>()
|
|
const [rerollPopupHost, showRerollPopup] = usePopup<HTMLDivElement>()
|
|
const copyWrapper = useCallback((format: ExportFormat) => {
|
|
if (!onCopy) {
|
|
console.error("No copy handler")
|
|
return showCopyPopup(`Failed to copy ${exportFormatToString(format)} to clipboard`, 'error')
|
|
}
|
|
onCopy(format).then(() => {
|
|
return showCopyPopup(`Copied ${exportFormatToString(format)} to clipboard`)
|
|
}).catch((ex: unknown) => {
|
|
console.error(ex)
|
|
return showCopyPopup(`Failed to copy ${exportFormatToString(format)} to clipboard`, 'error')
|
|
}).catch((ex: unknown) => {
|
|
console.error(ex)
|
|
})
|
|
}, [showCopyPopup, onCopy])
|
|
const md = useCallback(() => copyWrapper(ExportFormat.Markdown), [copyWrapper])
|
|
const bb = useCallback(() => copyWrapper(ExportFormat.BBCode), [copyWrapper])
|
|
const emoji = useCallback(() => copyWrapper(ExportFormat.TextEmoji), [copyWrapper])
|
|
const text = useCallback(() => copyWrapper(ExportFormat.TextOnly), [copyWrapper])
|
|
const selected = elements.reduce<null|GeneratorSelectionState>((current, next) => {
|
|
if (next.selected === null) {
|
|
return current
|
|
}
|
|
switch (current) {
|
|
case GeneratorSelectionState.Partial:
|
|
return GeneratorSelectionState.Partial
|
|
case GeneratorSelectionState.None:
|
|
return next.selected ? GeneratorSelectionState.Partial : GeneratorSelectionState.None
|
|
case GeneratorSelectionState.All:
|
|
return next.selected ? GeneratorSelectionState.All : GeneratorSelectionState.Partial
|
|
case null:
|
|
return next.selected ? GeneratorSelectionState.All : GeneratorSelectionState.None
|
|
}
|
|
}, null)
|
|
const selectAll = useCallback((ev: Event) => {
|
|
if (!onSelect || (ev.currentTarget instanceof HTMLButtonElement && ev.currentTarget.disabled)) {
|
|
return
|
|
}
|
|
onSelect(GeneratorSelect.All)
|
|
}, [onSelect, showRerollPopup])
|
|
const selectNone = useCallback((ev: Event) => {
|
|
if (!onSelect || (ev.currentTarget instanceof HTMLButtonElement && ev.currentTarget.disabled)) {
|
|
return
|
|
}
|
|
onSelect(GeneratorSelect.None)
|
|
}, [onSelect, showRerollPopup])
|
|
const rerollSelected = useCallback((ev: Event) => {
|
|
if (!onReroll || (ev.currentTarget instanceof HTMLButtonElement && ev.currentTarget.disabled)) {
|
|
return
|
|
}
|
|
onReroll(GeneratorReroll.Selected).then(() => {}).catch((ex: unknown) => {
|
|
console.error(ex)
|
|
return showRerollPopup(`Failed to reroll`, 'error')
|
|
}).catch((ex: unknown) => {
|
|
console.error(ex)
|
|
})
|
|
}, [onReroll, showRerollPopup])
|
|
const rerollAll = useCallback((ev: Event) => {
|
|
if (!onReroll || (ev.currentTarget instanceof HTMLButtonElement && ev.currentTarget.disabled)) {
|
|
return
|
|
}
|
|
onReroll(GeneratorReroll.All).then(() => {}).catch((ex: unknown) => {
|
|
console.error(ex)
|
|
return showRerollPopup(`Failed to reroll all`, 'error')
|
|
}).catch((ex: unknown) => {
|
|
console.error(ex)
|
|
})
|
|
}, [onReroll, showRerollPopup])
|
|
|
|
return <div id="generator" class="page">
|
|
<form method="post" action={generatorTargetUrl} id="generatorWindow" class="window readable">
|
|
<h2 id="generatorHead">Your generated scenario</h2>
|
|
<ul id="generatedScenario">
|
|
{elements.map(i => <GeneratedElement key={tableIdentifier(i.table)} {...i} onSelectionChange={onSelectionChange} />)}
|
|
</ul>
|
|
<div id="generatorControls">
|
|
<div ref={copyPopupHost} id="copyButtons" className="buttons requiresJs jsPopupHost">
|
|
<FormButton id="copyMD" type="button" onClick={onCopy && md} disabled={!onCopy}>Markdown</FormButton>
|
|
<FormButton id="copyBB" type="button" onClick={onCopy && bb} disabled={!onCopy}>BBCode</FormButton>
|
|
<FormButton id="copyEmojiText" type="button" onClick={onCopy && emoji} disabled={!onCopy}>Text + Emoji</FormButton>
|
|
<FormButton id="copyText" type="button" onClick={onCopy && text} disabled={!onCopy}>Text Only</FormButton>
|
|
</div>
|
|
{editable ? <div ref={rerollPopupHost} id="rollButtons" class="buttons jsPopupHost">
|
|
<FormButton type="submit" id="reroll" name="submit"
|
|
value="reroll" disabled={onReroll && (!selected || selected === GeneratorSelectionState.None)} onClick={onReroll && rerollSelected}>
|
|
Reroll {selected === GeneratorSelectionState.All ? 'All' : 'Selected'}
|
|
</FormButton>
|
|
<FormButton type="button" id="selectAll" class="requiresJs" onClick={selectAll} disabled={!onSelect || selected === GeneratorSelectionState.All}>Select All</FormButton>
|
|
<FormButton type="button" id="selectNone" class="requiresJs" onClick={selectNone} disabled={!onSelect || !selected || selected === GeneratorSelectionState.None}>Select None</FormButton>
|
|
</div> : null}
|
|
<div id="scenarioButtons" class="buttons">
|
|
{editable ? <Fragment>
|
|
<LinkButton id="rerollAll" href={generatorTargetUrl} external={false} onClick={onReroll && rerollAll}>New Scenario</LinkButton>
|
|
<FormButton id="saveScenario" name="submit" value="saveScenario" type="submit">Get Scenario Link</FormButton>
|
|
</Fragment> : <Fragment>
|
|
<LinkButton id="openInGenerator" href={generatorTargetUrl} external={false}>Open in Generator</LinkButton>
|
|
</Fragment>}
|
|
</div>
|
|
{ addToDiscordUrl || includesResponses ?
|
|
<div id="generatorLinks" class="buttons">
|
|
{addToDiscordUrl && <LinkButton external={true} id="addToDiscord" href={addToDiscordUrl}>Add to Discord</LinkButton>}
|
|
{includesResponses ? <LinkButton external={false} id="goToResponses" href="#responses">View Possible Responses</LinkButton> : null}
|
|
</div> : null}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
}
|
|
|