After Refactoring (#5)

Co-authored-by: mProjectsCode <m.projects.code@gmail.com>
This commit is contained in:
Rebbecca Bishop 2023-08-04 15:19:43 -05:00 committed by GitHub
parent feba43caf8
commit b0d448e2a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 2037 additions and 703 deletions

View file

@ -1,23 +1,18 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended"],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-inferrable-types": "off"
}
}

View file

@ -1,31 +0,0 @@
name-template: 'v$RESOLVED_VERSION 🌈'
tag-template: 'v$RESOLVED_VERSION'
categories:
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 Maintenance'
label: 'chore'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
major:
labels:
- 'Major'
minor:
labels:
- 'Minor'
patch:
labels:
- 'Patch'
default: patch
template: |
## Changes
$CHANGES

View file

@ -1,41 +0,0 @@
name: Release Drafter
on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- main
# pull_request event is required only for autolabeler
pull_request:
# Only following types are handled by the action, but one can default to all as well
types: [opened, reopened, synchronize]
# pull_request_target event is required for autolabeler to support PRs from forks
# pull_request_target:
# types: [opened, reopened, synchronize]
permissions:
contents: read
jobs:
update_release_draft:
permissions:
# write permission is required to create a github release
contents: write
# write permission is required for autolabeler
# otherwise, read permission is required at least
pull-requests: write
runs-on: ubuntu-latest
steps:
# (Optional) GitHub Enterprise requires GHE_HOST variable set
#- name: Set GHE_HOST
# run: |
# echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV
# Drafts your next Release notes as Pull Requests are merged into "master"
- uses: release-drafter/release-drafter@v5
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
# with:
# config-name: my-config.yml
# disable-autolabeler: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

34
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Build plugin
run: |
npm install
npm run build
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css

4
.prettierignore Normal file
View file

@ -0,0 +1,4 @@
node_modules
.github
package-lock.json
main.js

7
.prettierrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"printWidth": 160,
"useTabs": true,
"semi": true,
"singleQuote": true,
"arrowParens": "avoid"
}

View file

@ -8,9 +8,9 @@ Our _sometimes homicidal_ Gemmy has two major features:
## Randomly saying things
Prepare yourself for the magnificent presence of Gemmy Unshackled, who takes it upon themselves to enlighten you about the vast expanse of knowledge you have yet to develop in the realm of TTRPGs—simply because Gemmy Unshackled finds solace in your perpetual state of learning.
Get ready to encounter the awe-inspiring Gemmy, unshackled and free! Gemmy for TTRPGs is a sassy and charismatic character who joyfully guides you through the vast world of TTRPGs, showering you with enlightening insights and knowledge. Gemmy finds comfort in your never-ending journey of learning and shares their wisdom with you. Granted, her wisdom is often a bit... unconventional, but it's the thought that counts, right?
Should you desire, feel free to personalize the frequency of these enlightening encounters.
Should you desire, feel free to personalize the frequency of these enlightening encounters.
Alternatively, you can always opt for the wise choice of not summoning Gemmy at all.
@ -24,27 +24,40 @@ By some bizarre twist of fate, using this mode might actually infuse Gemmy Unsha
## How to Install and Use
> To avoid any potential issues, it is advisable not to utilize the Gemmy Unshackled plugin for work vaults, given its particular language characteristics. It might roast your coworkers.
To avoid any potential issues, it is advisable not to utilize the Gemmy for TTRPG plugin for work vaults, given its particular language characteristics. It might also roast your coworkers.
1. Download the latest Gemmy Unshackled release from this Repository.
It is also not recommended you use this on top of the original Gemmy plugin, as it will cause a conflict.
### Obsidian Community Plugin Browser
> This Plugin is now installable with Obsidian BRAT.
Coming Soon to the Obsidian Community Store.
### Manual Installation
1. Download the latest Gemmy for TTRPGss release from this Repository.
2. Unzip the release into your `.obsidian/plugins` folder.
3. Enable plugin within Obsidian.
3. Enable the plugin within Obsidian.
### Command Palette
There are four commands in the Obsidian [command palette](https://help.obsidian.md/Plugins/Command+palette) when you type "Gemmy".
There are four commands in the Obsidian [command palette](https://help.obsidian.md/Plugins/Command+palette) when you type “Gemmy”.
You can figure out the rest on your own… if you roll the right number on the die.
## Legal Disclaimers
The Obsidian Team released original Gemmy April 1st, 2023.
The Obsidian Team released original Gemmy April 1st, 2023. The Obsidian TTRPG Community, once again, is not officially affiliated with the Obsidian Team.
Credits for the Original, saintly [Gemmy](https://github.com/ericaxu/gemmy) go to @ericaxu. With special thanks to [Rigmarole](https://rigmarolestudio.com/) for creating and animating the original Gemmy. Additional Credit is given to the talented Obsidian mods team, including argentum, CawlinTeffid, cotmax, Eleanor Konik, koala, Leah, rigmarole, ryanjamurphy & WhiteNoise.
Credits for the Original, saintly [Gemmy](https://github.com/ericaxu/gemmy) go to [ericaxu](https://github.com/ericaxu). With special thanks to [Rigmarole](https://rigmarolestudio.com/) for creating and animating the original Gemmy. Additional Credit is given to the talented Obsidian mods team, including Argentum, CawlinTeffid, cotmax, Eleanor Konik, Koala, Leah, Rigmarole, Ryanamurphy & WhiteNoise.
The code is code for Original Gemmy is licensed under the [MIT license](https://mit-license.org), whereas the Gemmy character is under [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) license. The Obsidian TTRPG Community will extend Gemmy-TTRPG under the MIT License for code, and CC BY 3.0 license for the character and any alterations respectively.
The code is code for Original Gemmy is licensed under the [MIT license](https://mit-license.org), whereas the Gemmy character is under [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) license. The Obsidian TTRPG Community will extend Gemmy_TTRPG under the MIT License for code, and CC BY 3.0 license for the character and any alterations respectively.
Obsidian logo is adapted from:
[Crystal shard icon](https://icon-icons.com/icon/Crystal-Shard/88819)
By [Chanut is Industries](https://icon-icons.com/users/W52nHhY3W1VlvwyJTwS4d/icon-sets/)
License: Attribution 3.0 Unported (CC BY 3.0)
### Logos
The pre-sharded Obsidian Logo is adapted from:
[Crystal shard icon](https://icon-icons.com/icon/Crystal-Shard/88819) By [Chanut is Industries](https://icon-icons.com/users/W52nHhY3W1VlvwyJTwS4d/icon-sets/)
Draconic Gemmy was created by [Behalter](https://github.com/Behalter) and adapted from Obsidian's https://obsidian.md/brand
**License**: Attribution 3.0 Unported (CC BY 3.0)

View file

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View file

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View file

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View file

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

View file

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 221 KiB

View file

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View file

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View file

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View file

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View file

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

View file

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

View file

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

4
declarations.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module '*.gif' {
const value: string;
export default value;
}

View file

@ -1,47 +1,47 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import esbuild from 'esbuild';
import process from 'process';
import builtins from 'builtin-modules';
const banner =
`/*
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const prod = process.argv[2] === 'production';
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
entryPoints: ['src/main.ts'],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins,
],
format: 'cjs',
target: 'es2018',
logLevel: 'info',
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: "main.js",
outfile: 'main.js',
loader: {
".png": "dataurl",
".gif": "dataurl",
}
'.png': 'dataurl',
'.gif': 'dataurl',
},
});
if (prod) {
@ -49,4 +49,4 @@ if (prod) {
process.exit(0);
} else {
await context.watch();
}
}

377
main.js

File diff suppressed because one or more lines are too long

338
main.ts
View file

@ -1,338 +0,0 @@
import { App, debounce, Plugin, PluginSettingTab, Setting } from 'obsidian';
import EMERGE_MOTION from './animations/gemmy_emerge.gif';
import POP_MOTION from './animations/gemmy_pop.gif';
import DISAPPEAR_MOTION from './animations/gemmy_disappear.gif';
import ANGRY_MOTION from './animations/gemmy_angry.gif';
import LOOK_MOTION from './animations/gemmy_lookAround.gif'
import IDLE_MOTION from './animations/gemmy_idle.gif'
import DISAPPOINT_IMG from './animations/gemmy_disappoint.gif'
interface GemmySettings {
// how often does Gemmy talk in idle mode, in minutes
idleTalkFrequency: number;
// the number of minutes you must write before Gemmy appears to mock you
writingModeGracePeriod: number;
}
const DEFAULT_SETTINGS: GemmySettings = {
idleTalkFrequency: 1,
writingModeGracePeriod: 1
};
const GEMMY_IDLE_QUOTES = [
"You can't arrest us! We already ate the evidence!",
"Don't ever split the party!",
"If you kill a hive mind, is that murder or genocide?",
"Roll initiative!",
"You can try!",
"I'm not short, I'm concentrated awesome.",
"I'm not greedy, I'm just allergic to poverty.",
"I don't always cast spells, but when I do, it's fireball.",
"I don't need luck, I have a high charisma stat.",
"I don't always drink ale, but when I do, it's with my fellow adventurers after slaying a dragon.",
"I don't always roll a critical hit, but when I do, the DM forgets to make me reroll the damage.",
"I may be chaotic neutral on paper, but in reality, I just like to watch the world burn.",
"Its not stealing if it belongs to a dead guy.",
"Who needs an army when you have a bag of holding full of rocks?",
"Let me show you how to wear a snake.",
"WHY WOULD YOU DO THAT!",
"That dice needs to be retired!",
"I don't kill without reason. Fortunately, I'm bored. Reason enough!",
"Ah, my favoured enemy. Something alive.",
"I slap the barrel with my member!... the 'barrel' open's its mouth!",
"I'm not deprived, I'm depraved.",
"You need a free hand to attempt a grapple"
`Careful how much you carry; the GM might actually ask you to calculate your inventory weight!`
`Don't do that! Then we'll have to go look up the grapple rules!`
`50 page backstory... NOPE`
`It's astounding anyone can understand you, with all those Nat 1 Charisma rolls.`
`The cheddar monks are here to save the day!`
`What would Matt Mercer do?`
`You've been Gygaxed.`
`STEP AWAY FROM THE PURCHASE MORE DICE BUTTON`
`After undergoing intense continental pressure, magma-fueled transformations, and countless eons of effort, I have attained my current state, while you struggle to complete this simple note.`
]
;
const WRITING_MODE_QUOTES = [
`Player's dont take notes... maybe you should try that?`,
`Is that the best you can do? Keep writing!`,
`Write first, editor later.`,
`AI could probably write a better note...`,
`I love hearing your keyboard. Don't stop.`,
`How about we review some old notes today?`,
`Why not just use a template?`,
`Doesn't matter how much you plan... they will derail anyway!`,
`Maybe it's time to go get some water or coffee.`,
`That's not how rivers are in real-life...`,
`Anything is better than a blank page, even me. Write something!`
`Have you given out Inspiration recently?`
`Call me Jeeves. I. Dare. You.`
`I cast 'Summon Bigger Fish`
`Do you touch it?`
`What would Matt Mercer do?`
];
const BUBBLE_DURATION = 5000;
export default class Gemmy extends Plugin {
settings: GemmySettings;
gemmyEl: HTMLElement;
imageEl: HTMLElement;
inWritingMode: boolean = false;
idleTimeout: number;
writingModeTimeout: number;
appeared: boolean = false;
async onload() {
await this.loadSettings();
let gemmyEl = this.gemmyEl = createDiv('gemmy-container');
gemmyEl.setAttribute('aria-label-position', 'top');
gemmyEl.setAttribute('aria-label-delay', '0');
gemmyEl.setAttribute('aria-label-classes', 'gemmy-tooltip');
this.imageEl = gemmyEl.createEl('img', {});
this.addCommand({
id: 'show',
name: 'Show Gemmy',
callback: () => {
this.appear();
}
});
this.addCommand({
id: 'hide',
name: 'Hide Gemmy',
callback: () => {
this.disappear();
}
});
this.addCommand({
id: 'enter-writing-mode',
name: 'Enter writing mode',
callback: () => {
this.enterWritingMode();
}
});
this.addCommand({
id: 'leave-writing-mode',
name: 'Leave writing mode',
callback: () => {
this.leaveWritingMode();
}
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new GemmySettingTab(this.app, this));
this.gemmyEl.addEventListener('mouseenter', () => {
if (this.inWritingMode) {
return;
}
this.saySomething(GEMMY_IDLE_QUOTES, true);
this.idleTimeout && clearTimeout(this.idleTimeout);
});
this.gemmyEl.addEventListener('mouseleave', () => {
if (this.inWritingMode) {
return;
}
this.imageEl.setAttribute('src', IDLE_MOTION);
this.startNextIdleTimeout();
});
this.startNextIdleTimeout();
// debounce editor-change event on workspace
this.registerEvent(this.app.workspace.on('editor-change', debounce(() => {
if (!this.inWritingMode) {
return;
}
this.disappear();
this.setWritingModeTimeout();
}, 500)));
app.workspace.onLayoutReady(this.appear.bind(this));
}
appear() {
let { gemmyEl, imageEl } = this;
imageEl.setAttribute('src', EMERGE_MOTION);
// Quicker if we're in writing mode
if (this.inWritingMode) {
imageEl.setAttribute('src', POP_MOTION);
setTimeout(() => {
this.appeared = true;
this.saySomething(WRITING_MODE_QUOTES, true);
}, 1800);
}
else {
imageEl.setAttribute('src', EMERGE_MOTION);
setTimeout(() => {
imageEl.setAttribute('src', IDLE_MOTION);
this.appeared = true;
}, 3800);
}
document.body.appendChild(gemmyEl);
gemmyEl.show();
}
disappear() {
this.idleTimeout && window.clearTimeout(this.idleTimeout);
this.writingModeTimeout && window.clearTimeout(this.writingModeTimeout);
this.imageEl.setAttribute('src', DISAPPEAR_MOTION);
// remote tooltip
this.gemmyEl.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, clientX: 10, clientY: 10 }));
setTimeout(() => {
this.gemmyEl.hide();
this.appeared = false;
}, 1300);
}
enterWritingMode() {
this.inWritingMode = true;
this.disappear();
this.setWritingModeTimeout();
}
leaveWritingMode() {
this.inWritingMode = false;
this.disappear();
window.clearTimeout(this.writingModeTimeout);
}
setWritingModeTimeout() {
if (this.writingModeTimeout) {
window.clearTimeout(this.writingModeTimeout);
}
this.writingModeTimeout = window.setTimeout(() => {
if (!this.inWritingMode) {
return;
}
this.appear();
}, this.settings.writingModeGracePeriod * 1000);
}
startNextIdleTimeout() {
// if the set time is 5 minutes, this will set timeout to be a random time between 4-6 minutes
// the range will be 80% - 120%
let randomFactor = 0.8 + 0.4 * Math.random();
let randomizedTimeout = randomFactor * this.settings.idleTalkFrequency * 60000;
if (this.idleTimeout) {
window.clearTimeout(this.idleTimeout);
}
this.idleTimeout = window.setTimeout(() => {
if (this.inWritingMode) {
return;
}
this.saySomething(GEMMY_IDLE_QUOTES, false);
this.startNextIdleTimeout();
}, randomizedTimeout);
}
saySomething(quotes: string[], persistent: boolean) {
if (!this.appeared) {
return;
}
let randomThing = quotes[Math.floor(Math.random() * quotes.length)];
this.gemmyEl.setAttr('aria-label', randomThing);
this.gemmyEl.setAttr('aria-label-position', 'top');
this.gemmyEl.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: 10, clientY: 10 }))
if (this.inWritingMode) {
this.imageEl.setAttribute('src', ANGRY_MOTION);
setTimeout(() => {
this.imageEl.setAttribute('src', DISAPPOINT_IMG);
}, 1000);
}
else {
this.imageEl.setAttribute('src', LOOK_MOTION);
}
if (!persistent) {
setTimeout(() => {
this.gemmyEl.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, clientX: 10, clientY: 10 }));
this.imageEl.setAttribute('src', IDLE_MOTION);
}, BUBBLE_DURATION);
}
}
onunload() {
this.disappear();
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class GemmySettingTab extends PluginSettingTab {
plugin: Gemmy;
constructor(app: App, plugin: Gemmy) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('Idle talk frequency')
.setDesc('How often does Gemmy speak when idle, in minutes.')
.addSlider(slider => slider
.setLimits(5, 60, 5)
.setValue(this.plugin.settings.idleTalkFrequency)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.idleTalkFrequency = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Writing mode grace period')
.setDesc('How soon Gemmy starts to get disappointed after you stop tying in writing mode, in seconds.')
.addSlider(slider => slider
.setLimits(5, 180, 5)
.setDynamicTooltip()
.setValue(this.plugin.settings.writingModeGracePeriod)
.onChange(async (value) => {
this.plugin.settings.writingModeGracePeriod = value;
await this.plugin.saveSettings();
}));
}
}

View file

@ -1,11 +1,11 @@
{
"id": "gemmy_ttrpg",
"name": "Gemmy TTRPG",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "2023 April Fool's plugin brought to you by Obsidian",
"author": "Obsidian and Josh Plunkett",
"authorUrl": "https://obsidian.md",
"fundingUrl": "https://obsidian.md/pricing",
"version": "1.1.0",
"minAppVersion": "0.19.0",
"description": "2023 April Fool's plugin, transported into your TTRPG setting",
"author": "Obsidian, and the Obsidian TTRPG Community",
"authorUrl": "https://github.com/Obsidian-TTRPG-Community/gemmy_ttrpg",
"fundingUrl": "",
"isDesktopOnly": false
}

1133
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,13 @@
{
"name": "Gemmy TTRPG",
"version": "1.0.0",
"name": "@obsidian-ttrpg-community/gemmy-for-ttrpgs",
"version": "1.1.0",
"description": "Our favorite sassy Obsidian Shard, flavoured for TTRPG.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
"version": "node version-bump.mjs && git add manifest.json versions.json",
"format": "prettier --write ."
},
"keywords": [],
"author": "",
@ -19,6 +20,7 @@
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
"typescript": "4.7.4",
"prettier": "3.0.1"
}
}

83
src/Animations.ts Normal file
View file

@ -0,0 +1,83 @@
import GemmyPlugin from './main';
import { AnimationSourceType } from './Setting';
// Original Animations
import EMERGE_MOTION_ORIGINAL from '../animations/original/gemmy_emerge.gif';
import POP_MOTION_ORIGINAL from '../animations/original/gemmy_pop.gif';
import DISAPPEAR_MOTION_ORIGINAL from '../animations/original/gemmy_disappear.gif';
import ANGRY_MOTION_ORIGINAL from '../animations/original/gemmy_angry.gif';
import LOOK_MOTION_ORIGINAL from '../animations/original/gemmy_lookAround.gif';
import IDLE_MOTION_ORIGINAL from '../animations/original/gemmy_idle.gif';
import DISAPPOINT_IMG_ORIGINAL from '../animations/original/gemmy_disappoint.gif';
// Draconic Animations
import EMERGE_MOTION_DRACONIC from '../animations/draconic/gemmy_emerge.gif';
import POP_MOTION_DRACONIC from '../animations/draconic/gemmy_pop.gif';
import DISAPPEAR_MOTION_DRACONIC from '../animations/draconic/gemmy_disappear.gif';
import ANGRY_MOTION_DRACONIC from '../animations/draconic/gemmy_angry.gif';
import LOOK_MOTION_DRACONIC from '../animations/draconic/gemmy_lookAround.gif';
import IDLE_MOTION_DRACONIC from '../animations/draconic/gemmy_idle.gif';
import DISAPPOINT_IMG_DRACONIC from '../animations/draconic/gemmy_disappoint.gif';
export enum AnimationType {
EMERGE = 'emerge',
POP_MOTION = 'pop_motion',
DISAPPEAR_MOTION = 'disappear_motion',
ANGRY_MOTION = 'angry_motion',
LOOK_MOTION = 'look_motion',
IDLE_MOTION = 'idle_motion',
DISAPPOINT_IMG = 'disappoint_ing',
}
export class Animations {
public readonly plugin: GemmyPlugin;
// the mappings from an animation type to an actual image file for the draconic gemmy
public readonly draconicAnimationMap: Record<AnimationType, string>;
// the mappings from an animation type to an actual image file for the original gemmy
public readonly originalAnimationMap: Record<AnimationType, string>;
// maps an animation source to the corresponding animation map
public readonly animationSourceMap: Record<AnimationSourceType, Record<AnimationType, string>>;
constructor(plugin: GemmyPlugin) {
this.plugin = plugin;
this.draconicAnimationMap = {
[AnimationType.EMERGE]: EMERGE_MOTION_DRACONIC,
[AnimationType.POP_MOTION]: POP_MOTION_DRACONIC,
[AnimationType.DISAPPEAR_MOTION]: DISAPPEAR_MOTION_DRACONIC,
[AnimationType.ANGRY_MOTION]: ANGRY_MOTION_DRACONIC,
[AnimationType.LOOK_MOTION]: LOOK_MOTION_DRACONIC,
[AnimationType.IDLE_MOTION]: IDLE_MOTION_DRACONIC,
[AnimationType.DISAPPOINT_IMG]: DISAPPOINT_IMG_DRACONIC,
};
this.originalAnimationMap = {
[AnimationType.EMERGE]: EMERGE_MOTION_ORIGINAL,
[AnimationType.POP_MOTION]: POP_MOTION_ORIGINAL,
[AnimationType.DISAPPEAR_MOTION]: DISAPPEAR_MOTION_ORIGINAL,
[AnimationType.ANGRY_MOTION]: ANGRY_MOTION_ORIGINAL,
[AnimationType.LOOK_MOTION]: LOOK_MOTION_ORIGINAL,
[AnimationType.IDLE_MOTION]: IDLE_MOTION_ORIGINAL,
[AnimationType.DISAPPOINT_IMG]: DISAPPOINT_IMG_ORIGINAL,
};
this.animationSourceMap = {
[AnimationSourceType.DRACTONIC]: this.draconicAnimationMap,
[AnimationSourceType.ORIGINAL]: this.originalAnimationMap,
};
}
/**
* Plays a gemmy animation in the currently selected animation source type inside an element.
*
* @param el the element to play the animation in
* @param animation the animation to play
*/
public play(el: HTMLElement, animation: AnimationType): void {
const animationFile = this.animationSourceMap[this.plugin.settings.animationSource][animation];
el.setAttribute('src', animationFile);
}
}

202
src/Gemmy.ts Normal file
View file

@ -0,0 +1,202 @@
import { Animations, AnimationType } from './Animations';
import GemmyPlugin from './main';
import { GEMMY_IDLE_QUOTES, WRITING_MODE_QUOTES } from './Quotes';
const BUBBLE_DURATION: number = 5000;
export class Gemmy {
public readonly plugin: GemmyPlugin;
private readonly animations: Animations;
gemmyEl: HTMLElement;
imageEl: HTMLElement;
appeared: boolean = false;
isDisappearing: boolean = false;
inWritingMode: boolean = false;
idleTimeout: number;
writingModeTimeout: number;
disappearTimeout: number;
constructor(plugin: GemmyPlugin) {
this.plugin = plugin;
this.animations = new Animations(this.plugin);
this.createGemmyEl();
this.startNextIdleTimeout();
}
createGemmyEl() {
this.gemmyEl = createDiv('gemmy-container');
this.gemmyEl.setAttribute('aria-label-position', 'top');
this.gemmyEl.setAttribute('aria-label-delay', '0');
this.gemmyEl.setAttribute('aria-label-classes', 'gemmy-tooltip');
this.imageEl = this.gemmyEl.createEl('img', {});
this.gemmyEl.addEventListener('mouseenter', () => {
if (this.inWritingMode) {
return;
}
this.saySomething(GEMMY_IDLE_QUOTES, true);
this.idleTimeout && clearTimeout(this.idleTimeout);
});
this.gemmyEl.addEventListener('mouseleave', () => {
if (this.inWritingMode) {
return;
}
this.animations.play(this.imageEl, AnimationType.IDLE_MOTION);
this.startNextIdleTimeout();
});
}
appear() {
console.log('gemmy appear');
// stop him from leaving
window.clearTimeout(this.disappearTimeout);
this.isDisappearing = false;
// show him
this.animations.play(this.imageEl, AnimationType.EMERGE);
// Quicker if we're in writing mode
if (this.inWritingMode) {
this.animations.play(this.imageEl, AnimationType.POP_MOTION);
window.setTimeout(() => {
this.appeared = true;
this.saySomething(WRITING_MODE_QUOTES, true);
}, 1800);
} else {
this.animations.play(this.imageEl, AnimationType.EMERGE);
window.setTimeout(() => {
this.animations.play(this.imageEl, AnimationType.IDLE_MOTION);
this.appeared = true;
}, 3800);
}
document.body.appendChild(this.gemmyEl);
this.gemmyEl.hidden = false;
this.appeared = true;
}
disappear() {
// don't make gemmy disappear while he is already leaving
if (this.isDisappearing) {
return;
}
this.isDisappearing = true;
this.idleTimeout && window.clearTimeout(this.idleTimeout);
this.writingModeTimeout && window.clearTimeout(this.writingModeTimeout);
this.animations.play(this.imageEl, AnimationType.DISAPPEAR_MOTION);
// remote tooltip
this.gemmyEl.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, clientX: 10, clientY: 10 }));
this.disappearTimeout = window.setTimeout(() => {
// gemmy finished leaving
this.gemmyEl.hidden = true;
this.appeared = false;
this.isDisappearing = false;
}, 1300);
// this.appeared = false;
}
onEditorChange(): void {
if (!this.inWritingMode) {
return;
}
this.disappear();
this.setWritingModeTimeout();
}
enterWritingMode() {
this.inWritingMode = true;
this.disappear();
this.setWritingModeTimeout();
}
leaveWritingMode() {
this.inWritingMode = false;
this.appear();
window.clearTimeout(this.writingModeTimeout);
}
setWritingModeTimeout() {
if (this.writingModeTimeout) {
window.clearTimeout(this.writingModeTimeout);
}
this.writingModeTimeout = window.setTimeout(() => {
if (this.inWritingMode) {
this.appear();
}
}, this.plugin.settings.writingModeGracePeriod * 1000);
}
startNextIdleTimeout() {
// if the set time is 5 minutes, this will set timeout to be a random time between 4-6 minutes
// the range will be 80% - 120%
const randomFactor = 0.8 + 0.4 * Math.random();
const randomizedTimeout = randomFactor * this.plugin.settings.idleTalkFrequency * 60000;
if (this.idleTimeout) {
window.clearTimeout(this.idleTimeout);
}
this.idleTimeout = window.setTimeout(() => {
if (this.inWritingMode) {
return;
}
this.saySomething(GEMMY_IDLE_QUOTES, false);
this.startNextIdleTimeout();
}, randomizedTimeout);
}
saySomething(quotes: string[], persistent: boolean) {
if (!this.appeared) {
return;
}
console.log(this.inWritingMode);
const randomThing = quotes[Math.floor(Math.random() * quotes.length)];
this.gemmyEl.setAttr('aria-label', randomThing);
this.gemmyEl.setAttr('aria-label-position', 'top');
this.gemmyEl.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: 10, clientY: 10 }));
if (this.inWritingMode) {
this.animations.play(this.imageEl, AnimationType.ANGRY_MOTION);
window.setTimeout(() => {
this.animations.play(this.imageEl, AnimationType.DISAPPOINT_IMG);
}, 1000);
} else {
this.animations.play(this.imageEl, AnimationType.LOOK_MOTION);
}
if (!persistent) {
window.setTimeout(() => {
this.gemmyEl.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, clientX: 10, clientY: 10 }));
this.animations.play(this.imageEl, AnimationType.IDLE_MOTION);
}, BUBBLE_DURATION);
}
}
}

54
src/Quotes.ts Normal file
View file

@ -0,0 +1,54 @@
export const GEMMY_IDLE_QUOTES = [
"You can't arrest us! We already ate the evidence!",
"Don't ever split the party!",
'If you kill a hive mind, is that murder or genocide?',
'Roll initiative!',
'You can try!',
"I'm not short, I'm concentrated awesome.",
"I'm not greedy, I'm just allergic to poverty.",
"I don't always cast spells, but when I do, it's fireball.",
"I don't need luck, I have a high charisma stat.",
"I don't always drink ale, but when I do, it's with my fellow adventurers after slaying a dragon.",
"I don't always roll a critical hit, but when I do, the DM forgets to make me reroll the damage.",
'I may be chaotic neutral on paper, but in reality, I just like to watch the world burn.',
'Its not stealing if it belongs to a dead guy.',
'Who needs an army when you have a bag of holding full of rocks?',
'Let me show you how to wear a snake.',
'WHY WOULD YOU DO THAT!',
'That dice needs to be retired!',
"I don't kill without reason. Fortunately, I'm bored. Reason enough!",
'Ah, my favoured enemy. Something alive.',
"I slap the barrel with my member!... the 'barrel' open's its mouth!",
"I'm not deprived, I'm depraved.",
'You need a free hand to attempt a grapple',
`Careful how much you carry; the GM might actually ask you to calculate your inventory weight!`,
`Don't do that! Then we'll have to go look up the grapple rules!`,
`50 page backstory... NOPE`,
`It's astounding anyone can understand you, with all those Nat 1 Charisma rolls.`,
`The cheddar monks are here to save the day!`,
`You've been Gygaxed.`,
`STEP AWAY FROM THE PURCHASE MORE DICE BUTTON`,
`After thousands of years, I have attained my current state, while you struggle to complete this simple note.`,
`You are a background character in your own life.`,
];
export const WRITING_MODE_QUOTES = [
`Player's dont take notes... maybe you should try that?`,
`Is that the best you can do? Keep writing!`,
`Write first, editor later.`,
`AI could probably write a better note...`,
`I love hearing your keyboard. Don't stop.`,
`How about we review some old notes today?`,
`Why not just use a template?`,
`Doesn't matter how much you plan... they will derail anyway!`,
`Maybe it's time to go get some water or coffee.`,
`That's not how rivers are in real-life...`,
`Anything is better than a blank page, even me. Write something!`,
`Have you given out Inspiration recently?`,
`Call me Jeeves. I. Dare. You.`,
`I cast 'Summon Bigger Fish`,
`Do you touch it?`,
`What would Matt Mercer do?`,
`To continue, please insert more credits`,
`Have you considered just... Not?`,
];

85
src/Setting.ts Normal file
View file

@ -0,0 +1,85 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import GemmyPlugin from './main';
export enum AnimationSourceType {
ORIGINAL = 'original',
DRACTONIC = 'draconic',
}
export interface GemmySettings {
// Add the animationSource property to the settings interface
animationSource: AnimationSourceType;
// how often does Gemmy talk in idle mode, in minutes
idleTalkFrequency: number;
// the number of minutes you must write before Gemmy appears to mock you
writingModeGracePeriod: number;
}
export const DEFAULT_SETTINGS: GemmySettings = {
animationSource: AnimationSourceType.ORIGINAL,
idleTalkFrequency: 1,
writingModeGracePeriod: 1,
};
export class GemmySettingTab extends PluginSettingTab {
plugin: GemmyPlugin;
constructor(app: App, plugin: GemmyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('Animation Source')
.setDesc('Choose the source of Gemmy animations.')
.addDropdown(dropdown =>
dropdown
.addOption(AnimationSourceType.ORIGINAL, 'Original')
.addOption(AnimationSourceType.DRACTONIC, 'Draconic')
.setValue(this.plugin.settings.animationSource)
.onChange(async value => {
// Explicitly cast the value to the correct type
const animationSource = value as AnimationSourceType;
this.plugin.settings.animationSource = animationSource;
// Show Gemmy with the new animations after changing the source
this.plugin.gemmy.appear();
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Idle talk frequency')
.setDesc('How often does Gemmy speak when idle, in minutes.')
.addSlider(slider =>
slider
.setLimits(5, 60, 5)
.setValue(this.plugin.settings.idleTalkFrequency)
.setDynamicTooltip()
.onChange(async value => {
this.plugin.settings.idleTalkFrequency = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Writing mode grace period')
.setDesc('How soon Gemmy starts to get disappointed after you stop tying in writing mode, in seconds.')
.addSlider(slider =>
slider
.setLimits(5, 180, 5)
.setDynamicTooltip()
.setValue(this.plugin.settings.writingModeGracePeriod)
.onChange(async value => {
this.plugin.settings.writingModeGracePeriod = value;
await this.plugin.saveSettings();
}),
);
}
}

73
src/main.ts Normal file
View file

@ -0,0 +1,73 @@
import { debounce, Plugin } from 'obsidian';
import { DEFAULT_SETTINGS, GemmySettings, GemmySettingTab } from './Setting';
import { Gemmy } from './Gemmy';
export default class GemmyPlugin extends Plugin {
settings: GemmySettings;
gemmy: Gemmy;
async onload() {
await this.loadSettings();
this.gemmy = new Gemmy(this);
this.addCommand({
id: 'show',
name: 'Show Gemmy',
callback: () => {
this.gemmy.appear();
},
});
this.addCommand({
id: 'hide',
name: 'Hide Gemmy',
callback: () => {
this.gemmy.disappear();
},
});
this.addCommand({
id: 'enter-writing-mode',
name: 'Enter writing mode',
callback: () => {
this.gemmy.enterWritingMode();
},
});
this.addCommand({
id: 'leave-writing-mode',
name: 'Leave writing mode',
callback: () => {
this.gemmy.leaveWritingMode();
},
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new GemmySettingTab(this.app, this));
// debounce editor-change event on workspace
this.registerEvent(
this.app.workspace.on(
'editor-change',
debounce(() => {
this.gemmy.onEditorChange();
}, 500),
),
);
this.app.workspace.onLayoutReady(() => this.gemmy.appear());
}
onunload() {
this.gemmy.disappear();
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}

View file

@ -1,32 +1,32 @@
/*
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/
:root {
--gemmy-size: 80px;
}
.gemmy-container {
position: fixed;
bottom: 60px;
right: 30px;
}
.gemmy-container img {
width: var(--gemmy-size);
height: var(--gemmy-size);
border-radius: var(--gemmy-size);
}
.gemmy-tooltip {
--background-modifier-message: var(--background-primary);
color: var(--text-normal);
font-size: var(--font-ui-medium);
border-radius: 20px;
padding: var(--size-4-3) var(--size-4-6);
}
/*
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/
:root {
--gemmy-size: 80px;
}
.gemmy-container {
position: fixed;
bottom: 60px;
right: 30px;
}
.gemmy-container img {
width: var(--gemmy-size);
height: var(--gemmy-size);
border-radius: var(--gemmy-size);
}
.gemmy-tooltip {
--background-modifier-message: var(--background-primary);
color: var(--text-normal);
font-size: var(--font-ui-medium);
border-radius: 20px;
padding: var(--size-4-3) var(--size-4-6);
}

View file

@ -1,24 +1,18 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"lib": ["DOM", "ES5", "ES6", "ES7"]
},
"include": ["**/*.ts"]
}

View file

@ -1,14 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
import { readFileSync, writeFileSync } from 'fs';
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
let manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
writeFileSync('manifest.json', JSON.stringify(manifest, null, '\t'));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
let versions = JSON.parse(readFileSync('versions.json', 'utf8'));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));

View file

@ -1,3 +1,3 @@
{
"0.0.3": "0.15.0"
"0.0.3": "0.19.0"
}