From b02841736899e680403e1c784d02266c2d558b52 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 6 May 2024 01:55:26 +0800 Subject: [PATCH] Update --- .gitignore | 2 + src/settings/index.ts | 2 + src/utils/deck.ts | 63 ++++++++++++++ src/utils/helpers.ts | 11 +++ src/utils/index.ts | 1 + src/view/deck.ts | 80 ++++++++++++++++++ src/view/dice.ts | 55 +++++++++++++ src/view/index.ts | 185 ++++++++++-------------------------------- src/view/word.ts | 83 +++++++++++++++++++ styles.css | 62 ++++++++++++-- 10 files changed, 397 insertions(+), 147 deletions(-) create mode 100644 src/utils/deck.ts create mode 100644 src/view/deck.ts create mode 100644 src/view/dice.ts create mode 100644 src/view/word.ts diff --git a/.gitignore b/.gitignore index edd61d4..e31f2a5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ data.json # Exclude macOS Finder (System Explorer) View States .DS_Store + +Makefile diff --git a/src/settings/index.ts b/src/settings/index.ts index c28918f..f0f8e57 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -21,6 +21,8 @@ export class SoloToolkitSettingTab extends PluginSettingTab { containerEl.empty(); + containerEl.createEl("h1", { text: "Solo RPG Toolkit settings" }); + new Setting(containerEl) .setName("Custom tables folder") .setDesc("Additional can be created in this folder") diff --git a/src/utils/deck.ts b/src/utils/deck.ts new file mode 100644 index 0000000..c3c4121 --- /dev/null +++ b/src/utils/deck.ts @@ -0,0 +1,63 @@ +import { randomFrom } from "./dice"; +import { shuffle } from "./helpers"; + +const values = [ + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "J", + "Q", + "K", + "A", +]; + +const suits = ["heart", "diamond", "club", "spade"]; + +const max = values.length * suits.length; + +export const drawCard = () => `${randomFrom(values)} of ${randomFrom(suits)}`; + +type Card = [string, string]; + +export class Deck { + cards: Card[] = []; + public drawn: Card[] = []; + + constructor() { + this.shuffle(); + } + + draw(): Card { + if (!this.cards.length) this.shuffle(); + const [value, suit] = this.cards.pop() as Card; + this.drawn.push([value, suit]); + return [value, suit]; + } + + shuffle() { + this.cards = []; + this.drawn = []; + + for (const value of values) { + for (const suit of suits) { + this.cards.push([value, suit]); + } + } + + shuffle(this.cards); + } + + size() { + return max - this.drawn.length; + } + + max() { + return max; + } +} diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 9de474c..8dba157 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -5,3 +5,14 @@ export const capitalize = (value: string): string => { return ""; } }; + +export const shuffle = (arr: T[]) => { + let index = arr.length; + let random = 0; + + while (index != 0) { + random = Math.floor(Math.random() * index); + index--; + [arr[index], arr[random]] = [arr[random], arr[index]]; + } +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index bbcf646..3c7a1c4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ export * from "./dice"; +export * from "./deck"; export * from "./word"; diff --git a/src/view/deck.ts b/src/view/deck.ts new file mode 100644 index 0000000..d365fe2 --- /dev/null +++ b/src/view/deck.ts @@ -0,0 +1,80 @@ +import { ButtonComponent, setIcon, setTooltip } from "obsidian"; +import { SoloToolkitView as View } from "./index"; +import { Deck } from "../utils"; + +export class DeckView { + view: View; + deck: Deck; + countEl: HTMLElement; + deckBtnsEl: HTMLElement; + deckResultsEl: HTMLElement; + + constructor(view: View) { + this.view = view; + this.deck = new Deck(); + } + + create() { + if (this.view.isMobile) { + this.deckResultsEl = this.view.tabViewEl.createDiv("deck-results"); + } + + this.deckBtnsEl = this.view.tabViewEl.createDiv("deck-buttons"); + this.deckBtnsEl.empty(); + this.createDeckBtns(); + + if (!this.view.isMobile) { + this.deckResultsEl = this.view.tabViewEl.createDiv("deck-results"); + } + + this.repopulateResults(); + } + + addResult(value: string, suit: string, immediate = false) { + const elClass = ["deck-result"]; + if (immediate) elClass.push("shown"); + const el = this.deckResultsEl.createDiv(elClass.join(" ")); + const result = el.createSpan("deck-result-value"); + result.setText(value); + const type = el.createSpan("deck-result-type"); + setIcon(type, suit); + setTooltip(type, `${value} of ${suit}s`); + if (!immediate) { + setTimeout(() => { + el.classList.add("shown"); + }, 30); + } + } + + createDeckBtns() { + this.countEl = this.deckBtnsEl.createDiv("deck-size"); + this.updateCount(); + + new ButtonComponent(this.deckBtnsEl).setButtonText("Shuffle").onClick(() => { + this.deck.shuffle(); + this.deckResultsEl.empty(); + this.updateCount(); + }); + + new ButtonComponent(this.deckBtnsEl) + .setButtonText("Draw a card") + .onClick(() => { + const [value, suit] = this.deck.draw(); + this.addResult(value, suit); + this.updateCount(); + }); + } + + updateCount() { + if (this.countEl) { + this.countEl.setText(`${this.deck.size()} / ${this.deck.max()}`); + } + } + + repopulateResults() { + for (const drawn of this.deck.drawn) { + const [value, suit] = drawn; + this.addResult(value, suit, true); + } + } +} diff --git a/src/view/dice.ts b/src/view/dice.ts new file mode 100644 index 0000000..1d6eb63 --- /dev/null +++ b/src/view/dice.ts @@ -0,0 +1,55 @@ +import { ButtonComponent } from "obsidian"; +import { SoloToolkitView as View } from "./index"; +import { roll, rollIntervals } from "../utils"; + +export class DiceView { + view: View; + diceBtnsEl: HTMLElement; + + constructor(view: View) { + this.view = view; + } + + create() { + this.diceBtnsEl = this.view.tabViewEl.createDiv("dice-buttons"); + this.diceBtnsEl.empty(); + this.createDiceBtn(4); + this.createDiceBtn(6); + this.createDiceBtn(8); + this.createDiceBtn(10); + this.createDiceBtn(12); + this.createDiceBtn(20); + } + + addResult(container: HTMLElement, max: number) { + let value = roll(max); + const el = container.createDiv("dice-result-value"); + el.setText(value.toString()); + + let i = 0; + const reroll = () => { + value = roll(max, value); + el.setText(value.toString()); + i++; + if (rollIntervals[i]) { + setTimeout(reroll, rollIntervals[i]); + } + }; + setTimeout(reroll, rollIntervals[i]); + } + + createDiceBtn(d: number) { + const container = this.diceBtnsEl.createDiv( + `dice-variant dice-variant-${d}`, + ); + + const results = container.createDiv(`dice-results dice-results-${d}`); + + const button = new ButtonComponent(container) + .setIcon(`srt-d${d}`) + .setTooltip(`Roll d${d}`) + .onClick(() => { + this.addResult(results, d); + }); + } +} diff --git a/src/view/index.ts b/src/view/index.ts index f24ffcd..849d4ce 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -1,31 +1,29 @@ -import { - TFile, - ItemView, - ButtonComponent, - WorkspaceLeaf, - Platform, -} from "obsidian"; +import { ItemView, ButtonComponent, WorkspaceLeaf, Platform } from "obsidian"; +import { DiceView } from "./dice"; +import { DeckView } from "./deck"; +import { WordView } from "./word"; import { SoloToolkitSettings } from "../settings"; -import { roll, rollIntervals, generateWord, randomFrom } from "../utils"; export const VIEW_TYPE = "MAIN_VIEW"; export class SoloToolkitView extends ItemView { - settings: SoloToolkitSettings; - isMobile: boolean = false; + public settings: SoloToolkitSettings; + public isMobile: boolean = false; - tab: "dice" | "word" = "dice"; - tabEl: HTMLElement; + tab: "dice" | "deck" | "word" = "dice"; tabPickerEl: HTMLElement; + public tabViewEl: HTMLElement; - wordEl: HTMLElement; - wordResultsEl: HTMLElement; - - diceEl: HTMLElement; + dice: DiceView; + deck: DeckView; + word: WordView; constructor(leaf: WorkspaceLeaf, settings: SoloToolkitSettings) { super(leaf); this.settings = settings; + this.dice = new DiceView(this); + this.deck = new DeckView(this); + this.word = new WordView(this); } getViewType() { @@ -48,7 +46,7 @@ export class SoloToolkitView extends ItemView { (this.app as any).isMobile || Platform.isIosApp || Platform.isAndroidApp; this.containerEl = parent.createDiv("srt-container"); - this.tabEl = this.containerEl.createDiv("srt-tab"); + this.tabViewEl = this.containerEl.createDiv("srt-tab"); this.tabPickerEl = this.containerEl.createDiv("srt-tab-picker"); if (this.isMobile) { @@ -68,144 +66,49 @@ export class SoloToolkitView extends ItemView { createTabPicker() { this.tabPickerEl.empty(); - const wordBtn = new ButtonComponent(this.tabPickerEl).setButtonText( - "Ideas", - ); - const diceBtn = new ButtonComponent(this.tabPickerEl).setButtonText("Dice"); + const btns = { + word: new ButtonComponent(this.tabPickerEl).setButtonText("Ideas"), + deck: new ButtonComponent(this.tabPickerEl).setButtonText("Deck"), + dice: new ButtonComponent(this.tabPickerEl).setButtonText("Dice"), + }; - wordBtn.onClick(() => { + const setCta = (btn: keyof typeof btns) => { + for (const btn of Object.values(btns)) { + btn.removeCta(); + } + btns[btn].setCta(); + }; + + setCta(this.tab); + + btns.word.onClick(() => { this.tab = "word"; - wordBtn.setCta(); - diceBtn.removeCta(); + setCta("word"); this.createTab(); }); - diceBtn.onClick(() => { + btns.deck.onClick(() => { + this.tab = "deck"; + setCta("deck"); + this.createTab(); + }); + btns.dice.onClick(() => { this.tab = "dice"; - diceBtn.setCta(); - wordBtn.removeCta(); + setCta("dice"); this.createTab(); }); - - if (this.tab === "word") wordBtn.setCta(); - if (this.tab === "dice") diceBtn.setCta(); } createTab() { - this.tabEl.empty(); + this.tabViewEl.empty(); if (this.tab === "word") { - if (this.isMobile) { - this.wordResultsEl = this.tabEl.createDiv("word-results"); - } - - this.wordEl = this.tabEl.createDiv("word-container"); - this.wordEl.empty(); - this.createWord("Subject"); - this.createWord("Action"); - this.createWord("Name"); - this.createWord("Looks"); - this.createWord("Job"); - this.createWord("Town"); - - if (this.settings.customTableRoot) { - const files = this.app.vault.getMarkdownFiles(); - for (const file of files) { - if (file.parent?.path === this.settings.customTableRoot) { - this.createCustomWord(file); - } - } - } - - if (!this.isMobile) { - this.wordResultsEl = this.tabEl.createDiv("word-results"); - } + this.word.create(); + } + if (this.tab === "deck") { + this.deck.create(); } - if (this.tab === "dice") { - this.diceEl = this.tabEl.createDiv("dice-container"); - this.diceEl.empty(); - this.createDice(4); - this.createDice(6); - this.createDice(8); - this.createDice(10); - this.createDice(12); - this.createDice(20); + this.dice.create(); } } - - createDice(d: number) { - const container = this.diceEl.createDiv(`dice-variant dice-variant-${d}`); - - const results = container.createDiv(`dice-results dice-results-${d}`); - - const button = new ButtonComponent(container) - .setIcon(`srt-d${d}`) - .setTooltip(`Roll d${d}`) - .onClick(() => { - this.addDiceRoll(results, d); - }); - } - - addDiceRoll(container: any, max: number) { - let value = roll(max); - const el = container.createDiv("dice-result-value"); - el.setText(value); - - let i = 0; - const reroll = () => { - value = roll(max, value); - el.setText(value); - i++; - if (rollIntervals[i]) { - setTimeout(reroll, rollIntervals[i]); - } - }; - setTimeout(reroll, rollIntervals[i]); - } - - createWord(value: string) { - const button = new ButtonComponent(this.wordEl) - .setButtonText(value) - .setTooltip(`Generate ${value.toLowerCase()}`) - .onClick(() => { - const el = this.wordResultsEl.createDiv("word-result"); - const type = el.createSpan("word-result-type"); - type.setText(value); - const result = el.createSpan("word-result-value"); - result.setText(generateWord(value)); - setTimeout(() => { - el.classList.add("shown"); - }, 30); - }); - } - - createCustomWord(file: TFile) { - const value = file.basename; - let words: string[] = []; - - this.app.vault.cachedRead(file).then((content: string) => { - if (!content) return; - - words = content - .split("\n") - .map((line: string) => line.trim()) - .filter((line: string) => line); - }); - - const button = new ButtonComponent(this.wordEl) - .setButtonText(value) - .setTooltip(`Generate ${value.toLowerCase()}`) - .onClick(() => { - if (!words.length) return; - - const el = this.wordResultsEl.createDiv("word-result"); - const type = el.createSpan("word-result-type"); - type.setText(value); - const result = el.createSpan("word-result-value"); - result.setText(randomFrom(words)); - setTimeout(() => { - el.classList.add("shown"); - }, 30); - }); - } } diff --git a/src/view/word.ts b/src/view/word.ts new file mode 100644 index 0000000..8b0216f --- /dev/null +++ b/src/view/word.ts @@ -0,0 +1,83 @@ +import { TFile, ButtonComponent } from "obsidian"; +import { SoloToolkitView as View } from "./index"; +import { generateWord, randomFrom } from "../utils"; + +export class WordView { + view: View; + wordBtnsEl: HTMLElement; + wordResultsEl: HTMLElement; + + constructor(view: View) { + this.view = view; + } + + create() { + if (this.view.isMobile) { + this.wordResultsEl = this.view.tabViewEl.createDiv("word-results"); + } + + this.wordBtnsEl = this.view.tabViewEl.createDiv("word-buttons"); + this.wordBtnsEl.empty(); + this.createWordBtn("Subject"); + this.createWordBtn("Action"); + this.createWordBtn("Name"); + this.createWordBtn("Looks"); + this.createWordBtn("Job"); + this.createWordBtn("Town"); + + if (this.view.settings.customTableRoot) { + const files = this.view.app.vault.getMarkdownFiles(); + for (const file of files) { + if (file.parent?.path === this.view.settings.customTableRoot) { + this.createCustomWordBtn(file); + } + } + } + + if (!this.view.isMobile) { + this.wordResultsEl = this.view.tabViewEl.createDiv("word-results"); + } + } + + addResult(type: string, value: string) { + const el = this.wordResultsEl.createDiv("word-result"); + const typeEl = el.createSpan("word-result-type"); + typeEl.setText(type); + const valueEl = el.createSpan("word-result-value"); + valueEl.setText(value); + setTimeout(() => { + el.classList.add("shown"); + }, 30); + } + + createWordBtn(type: string) { + new ButtonComponent(this.wordBtnsEl) + .setButtonText(type) + .setTooltip(`Generate ${type.toLowerCase()}`) + .onClick(() => { + this.addResult(type, generateWord(type)); + }); + } + + createCustomWordBtn(file: TFile) { + const type = file.basename; + let values: string[] = []; + + this.view.app.vault.cachedRead(file).then((content: string) => { + if (!content) return; + + values = content + .split("\n") + .map((line: string) => line.trim()) + .filter((line: string) => line); + }); + + new ButtonComponent(this.wordBtnsEl) + .setButtonText(type) + .setTooltip(`Generate ${type.toLowerCase()}`) + .onClick(() => { + if (!values.length) return; + this.addResult(type, randomFrom(values)); + }); + } +} diff --git a/styles.css b/styles.css index ac26a84..a6b7265 100644 --- a/styles.css +++ b/styles.css @@ -14,6 +14,7 @@ flex-direction: row; align-items: center; justify-content: flex-end; + flex-wrap: wrap-reverse; gap: var(--size-4-4); width: 100%; padding-top: var(--size-4-4); @@ -32,7 +33,7 @@ overflow: hidden; } - .dice-container { + .dice-buttons { display: flex; flex-direction: column; gap: var(--size-4-4); @@ -56,12 +57,59 @@ } .dice-result-value { - width: 32px; + min-width: 32px; text-align: center; } } - .word-container { + .deck-buttons { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: var(--size-4-4); + white-space: nowrap; + + .deck-size { + line-height: var(--input-height); + } + } + + .deck-results { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + gap: var(--size-4-4); + width: 100%; + overflow: hidden; + + .deck-result { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: var(--size-4-2); + transition: opacity 0.3s ease-in-out; + } + + .deck-result:not(.shown) { + opacity: 0; + } + + .deck-result-value { + display: inline-block; + min-width: 20px; + text-align: right; + } + + .deck-result-type { + position: relative; + top: 2px; + } + } + + .word-buttons { display: flex; flex-direction: row; flex-wrap: wrap; @@ -98,9 +146,6 @@ text-align: right; margin-right: var(--size-4-4); } - - .word-result-type { - } } &.srt-desktop-layout { @@ -121,6 +166,11 @@ width: 100%; } + .deck-results { + flex-direction: column-reverse; + margin-bottom: var(--size-4-6); + } + .word-results { flex-direction: column-reverse; margin-bottom: var(--size-4-6);