This commit is contained in:
Alex 2024-05-06 01:55:26 +08:00
parent 3195e11377
commit b028417368
10 changed files with 397 additions and 147 deletions

2
.gitignore vendored
View file

@ -21,3 +21,5 @@ data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
Makefile

View file

@ -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")

63
src/utils/deck.ts Normal file
View file

@ -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;
}
}

View file

@ -5,3 +5,14 @@ export const capitalize = (value: string): string => {
return "";
}
};
export const shuffle = <T>(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]];
}
};

View file

@ -1,2 +1,3 @@
export * from "./dice";
export * from "./deck";
export * from "./word";

80
src/view/deck.ts Normal file
View file

@ -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);
}
}
}

55
src/view/dice.ts Normal file
View file

@ -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);
});
}
}

View file

@ -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);
});
}
}

83
src/view/word.ts Normal file
View file

@ -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));
});
}
}

View file

@ -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);