mirror of
https://github.com/alexkurowski/solo-toolkit.git
synced 2026-07-22 10:10:32 +00:00
Update
This commit is contained in:
parent
3195e11377
commit
b028417368
10 changed files with 397 additions and 147 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -21,3 +21,5 @@ data.json
|
|||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
|
||||
Makefile
|
||||
|
|
|
|||
|
|
@ -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
63
src/utils/deck.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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]];
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./dice";
|
||||
export * from "./deck";
|
||||
export * from "./word";
|
||||
|
|
|
|||
80
src/view/deck.ts
Normal file
80
src/view/deck.ts
Normal 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
55
src/view/dice.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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
83
src/view/word.ts
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
62
styles.css
62
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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue