Skip to content

Commit

Permalink
Add support for tables with dice rolls
Browse files Browse the repository at this point in the history
  • Loading branch information
rg-wood committed Apr 5, 2024
1 parent e53b139 commit 4183fce
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 10 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,40 @@ Simple, one-column table (elements are selected at random with equal weight):
</vellum-random-table>
```

Two-column table where items are selected by a dice roll:

```html
<vellum-random-table select="#result">
<caption>
Random Encounters
</caption>
<table>
<thead>
<tr>
<th>d6</th>
<th>Encounter</th>
</tr>
</thead>
<tbody>
<tr>
<td>1-3</td>
<td>1 wolf</td>
</tr>
<tr>
<td>4-5</td>
<td>2 goblins</td>
</tr>
<tr>
<td>6</td>
<td>dragon</td>
</tr>
</tbody>
</table>
<button>Roll</button>
<input id="result" type="text" />
</vellum-random-table>
```

## Installation

Install via [npm](https://www.npmjs.com/package/@daviddarnes/component-name): `npm i vellum-random-table -S`.
Expand Down
32 changes: 32 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,37 @@ <h2>One-column random table</h2>
<button>Roll</button>
<input id="result" type="text" />
</vellum-random-table>

<h2>Two-column random table with dice</h2>

<vellum-random-table select="#result">
<caption>
Random Encounters
</caption>
<table>
<thead>
<tr>
<th>d6</th>
<th>Encounter</th>
</tr>
</thead>
<tbody>
<tr>
<td>1-3</td>
<td>1 wolf</td>
</tr>
<tr>
<td>4-5</td>
<td>2 goblins</td>
</tr>
<tr>
<td>6</td>
<td>dragon</td>
</tr>
</tbody>
</table>
<button>Roll</button>
<input id="result" type="text" />
</vellum-random-table>
</body>
</html>
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vellum-random-table",
"version": "0.0.0",
"version": "0.1.0",
"description": "Web component for interactive random tables",
"author": "Ric Wood <ric@grislyeye.com>",
"repository": "https://github.com/grislyeye/vellum-random-table",
Expand Down
33 changes: 33 additions & 0 deletions src/dice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export class Die {
private static EMPTY_STR_TO_UNDEFINED = (str: string) =>
str === '' ? undefined : str

private notation: string
readonly number: number
readonly dice: number
readonly modifier: number

constructor(notation: string) {
this.notation = notation

const diceNotation: RegExp = /^(\d*)d(\d+)(\s*(\+|-)\s*(\d+))?$/g

const [, number = '1', dice = '1', , plusMinus = '+', modifier = '0'] =
diceNotation.exec(this.notation)!.map(Die.EMPTY_STR_TO_UNDEFINED)

this.number = parseInt(number)
this.dice = parseInt(dice)
this.modifier = parseInt(plusMinus + modifier)
}

roll(): number {
const rolls = Array.from({ length: this.number }, () =>
Math.floor(Math.random() * this.dice + 1),
)
return rolls.reduce((a, b) => a + b, 0) + this.modifier
}

toString(): string {
return this.notation
}
}
19 changes: 19 additions & 0 deletions src/range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type Range = number[]

const EMPTY_STR_TO_UNDEFINED = (str: string) => (str === '' ? undefined : str)

export function parseRange(notation: string): Range | undefined {
const rangeNotation: RegExp = /^(\d*)(\W?-(\W?\d*))?$/g
const [, start, , end] = rangeNotation
.exec(notation)!
.map(EMPTY_STR_TO_UNDEFINED)

if (start && !end) {
return [parseInt(start)]
} else if (start && end) {
const size = parseInt(end) - parseInt(start) + 1
return [...Array(size).keys()].map((i) => i + parseInt(start))
}

return
}
62 changes: 55 additions & 7 deletions src/vellum-random-table.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { LitElement, css, html } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { Die } from './dice'
import { Range, parseRange } from './range'

enum TableMode {
FirstColumn,
TwoColumn,
}

@customElement('vellum-random-table')
export class VellumRandomTable extends LitElement {
Expand All @@ -18,6 +25,14 @@ export class VellumRandomTable extends LitElement {
this.button.addEventListener('click', () => this.roll())
}

get mode(): TableMode {
if (this.table.rows[0].cells.length == 2) {
return TableMode.TwoColumn
} else {
return TableMode.FirstColumn
}
}

private get button(): HTMLButtonElement {
return this.querySelector('button') as HTMLButtonElement
}
Expand All @@ -26,10 +41,23 @@ export class VellumRandomTable extends LitElement {
return this.querySelector('table') as HTMLTableElement
}

private get selection(): string[] {
private get die(): Die | undefined {
const maybeDieNotation = this.table.rows[0].cells[0].textContent
if (maybeDieNotation) return new Die(maybeDieNotation)
return undefined
}

private ranges(column: number): Range[] {
return this.selection(column)
.map((content) => content.trim())
.map((cell) => parseRange(cell))
.filter((item): item is Range => !!item)
}

private selection(column: number): string[] {
return Array.from(this.table.tBodies)
.flatMap((tbody) => Array.from(tbody.rows))
.map((row) => row.cells[0])
.map((row) => row.cells[column])
.map((cell) => cell.textContent)
.map((content) => (content ? content : ''))
.map((content) => content.trim())
Expand All @@ -41,13 +69,33 @@ export class VellumRandomTable extends LitElement {
}

roll(): void {
if (this.resultTarget) {
const selection = this.selection
const target = this.resultTarget
if (!target) return

if (this.mode == TableMode.FirstColumn) {
const selection = this.selection(0)
const result = selection[Math.floor(Math.random() * selection.length)]
this.display(result)
} else if (this.mode == TableMode.TwoColumn) {
const ranges = this.ranges(0)
const selection = this.selection(1)

const roll = this.die?.roll()

if (roll) {
const index = ranges.findIndex((range) => range.includes(roll))

const result = selection[index]
this.display(`${result} (${roll})`)
}
}
}

if (this.resultTarget instanceof HTMLInputElement)
this.resultTarget.value = result
else this.resultTarget.textContent = result
private display(result: string): void {
if (this.resultTarget && this.resultTarget instanceof HTMLInputElement) {
this.resultTarget.value = result
} else if (this.resultTarget) {
this.resultTarget.textContent = result
}
}

Expand Down

0 comments on commit 4183fce

Please sign in to comment.