Initial Commit

This commit is contained in:
Arindy 2025-03-01 14:10:08 +01:00
parent fa5cb51858
commit ad372a6271
62 changed files with 4587 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
yarn.lock linguist-generated

1
.gitignore vendored
View File

@ -77,3 +77,4 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
.foundry

3750
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "foundryvtt-dice-tower",
"version": "1.0.0",
"description": "Roll your dice in a fancy overlay",
"license": "GPL-3.0-only",
"scripts": {
"clean": "rm -rf dist",
"build": "npm run clean && tsc && vite build",
"watch": "tsc && vite build --watch",
"copy": "sudo rsync -avP dist/ .foundry/data/Data/modules/dice-tower/",
"buildCopy": "npm run build && npm run copy"
},
"devDependencies": {
"@league-of-foundry-developers/foundry-vtt-types": "^12.331.3-beta",
"@3d-dice/dice-box-threejs": "^0.0.12",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-scss": "^4.0.1",
"sass": "^1.85.1",
"typescript": "5.4.5",
"vite": "^6.2.0"
}
}

29
src/languages/en.json Normal file
View File

@ -0,0 +1,29 @@
{
"DICETOWER.roll": "Roll",
"DICETOWER.d4": "D4",
"DICETOWER.d6": "D6",
"DICETOWER.d8": "D8",
"DICETOWER.d10": "D10",
"DICETOWER.d12": "D12",
"DICETOWER.d20": "D20",
"DICETOWER.d100": "D100",
"DICETOWER.addDice": "Add a dice",
"DICETOWER.removeDice": "Remove a dice",
"DICETOWER.rollD4": "Roll D4",
"DICETOWER.rollD6": "Roll D6",
"DICETOWER.rollD8": "Roll D8",
"DICETOWER.rollD10": "Roll D10",
"DICETOWER.rollD12": "Roll D12",
"DICETOWER.rollD20": "Roll D20",
"DICETOWER.rollD100": "Roll D100",
"DICETOWER.customizeTooltip": "Customize Dice",
"DICETOWER.rollFor": "Select actor to roll for",
"DICETOWER.openSheet": "Open character sheet",
"DICETOWER.customizeTitle": "Dice-Customization",
"DICETOWER.customize": "Dice-Customization",
"DICETOWER.customizeHint": "Customize your dice",
"DICETOWER.sendToDiceTowerName": "Send to dice-tower",
"DICETOWER.diceTowerUrlName": "URL to dice-tower",
"DICETOWER.diceTowerRoomName": "Name of Room in dice-tower",
"DICETOWER.overlaysTitle": "Overlay URLs"
}

25
src/module.json Normal file
View File

@ -0,0 +1,25 @@
{
"id": "dice-tower",
"title": "Dice-Tower",
"authors": [
{
"name": "Arindy"
}
],
"compatibility": {
"minimum": "12",
"verified": "12"
},
"socket": true,
"languages": [
{
"lang": "en",
"name": "English",
"path": "languages/en.json"
}
],
"license": "LICENSE",
"readme": "README.md",
"styles": ["styles/style.css"],
"esmodules": ["scripts/dice-tower.js"]
}

44
src/styles/style.scss Normal file
View File

@ -0,0 +1,44 @@
#dice-tower-controls {
flex: 0 0 30px;
margin: 6px 6px;
align-content: center;
position: relative;
}
#dice-tower-controls button {
border-radius: 10px;
line-height: 20px;
height: 100%;
margin: 2px 2px;
}
#dice-tower-controls label {
text-align: center;
margin: 6px 6px;
}
#dice-tower-controls select {
width: 200px;
height: 24px;
margin: 0 6px;
background: rgba(255, 255, 245, 0.8);
}
#dice-box {
position: relative;
justify-self: center;
box-sizing: border-box;
width: 600px;
height: 400px;
background: transparent;
background-size: cover;
}
#app {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
}

View File

@ -0,0 +1,31 @@
<div id="dice-tower-controls">
<div>
<select id="dice-tower-actor" data-tooltip="DICETOWER.rollFor"></select>
<a id="dice-tower-sheet" role="button" style="align-content: center; position: absolute; right: 52px; top: 4px;">
<i class="fas fa-address-book" data-tooltip="DICETOWER.openSheet"></i>
</a>
<a id="dice-tower-customize" role="button" style="align-content: center; position: absolute; right: 30px; top: 4px;">
<i class="fas fa-palette" data-tooltip="DICETOWER.customizeTooltip"></i>
</a>
<a id="dice-tower-urls" role="button" style="align-content: center; position: absolute; right: 6px; top: 4px;">
<i class="fas fa-link" data-tooltip="DICETOWER.overlaysTitle"></i>
</a>
</div>
<div class="flexrow" style="justify-self: center">
<label>{{localize 'DICETOWER.roll'}}</label>
<button id="dice-tower-remove-dice" data-tooltip="DICETOWER.removeDice">-</button>
<label id="dice-tower-amount">1</label>
<button id="dice-tower-add-dice" data-tooltip="DICETOWER.addDice">+</button>
</div>
<div class="flexrow">
<button id="dice-tower-roll-d4" data-tooltip="DICETOWER.rollD4">{{localize 'DICETOWER.d4'}}</button>
<button id="dice-tower-roll-d6" data-tooltip="DICETOWER.rollD6">{{localize 'DICETOWER.d6'}}</button>
<button id="dice-tower-roll-d8" data-tooltip="DICETOWER.rollD8">{{localize 'DICETOWER.d8'}}</button>
<button id="dice-tower-roll-d10" data-tooltip="DICETOWER.rollD10">{{localize 'DICETOWER.d10'}}</button>
<button id="dice-tower-roll-d12" data-tooltip="DICETOWER.rollD12">{{localize 'DICETOWER.d12'}}</button>
<button id="dice-tower-roll-d20" data-tooltip="DICETOWER.rollD20">{{localize 'DICETOWER.d20'}}</button>
<button id="dice-tower-roll-d100" data-tooltip="DICETOWER.rollD100">{{localize 'DICETOWER.d100'}}</button>
</div>
</div>`

View File

@ -0,0 +1,20 @@
<div id="dice-tower-controls">
<div style="text-align: center;">
<label for="theme">Theme </label>
<select name="theme" id="theme" style="margin: 0 25px"></select>
</div>
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline">
<div style="flex-grow: 1; padding: 0 10px">
<color-picker id="faceColor" name="Face" value="#8d8981"></color-picker>
</div>
<div style="flex-grow: 1; padding: 10px 0">
<color-picker id="numberColor" name="Numbers" value="black"></color-picker>
</div>
</div>
<div id="dice-box">
<div id="app"></div>
</div>
<button style="height: 25px" id="preview">Preview <i class="fa-solid fa-magnifying-glass"></i></button>
<button style="height: 25px" id="save">Save <i class="fa-solid fa-floppy-disk"></i></button>
</div>`

View File

@ -0,0 +1,5 @@
<div id="dice-tower-overlays">
<div id="dice-overlay"></div>
<hr>
<div id="result-overlay"></div>
</div>

BIN
src/textures/astral.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/textures/bronze01.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

BIN
src/textures/bronze02.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
src/textures/bronze03.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
src/textures/bronze03a.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
src/textures/bronze03b.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
src/textures/bronze04.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

BIN
src/textures/cheetah.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/textures/cloudy.alt.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
src/textures/cloudy.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
src/textures/dragon-bump.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
src/textures/dragon.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
src/textures/feather-bump.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
src/textures/feather.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
src/textures/fire.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/textures/glitter-alpha.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/textures/glitter-bump.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
src/textures/glitter.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/textures/ice.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
src/textures/leopard.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/textures/lizard-bump.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/textures/lizard.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/textures/marble.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
src/textures/metal-bump.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
src/textures/metal.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
src/textures/noise-thin-film.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src/textures/noise.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
src/textures/paper-bump.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/textures/paper.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/textures/skulls.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
src/textures/speckles.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
src/textures/stainedglass.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
src/textures/stars.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
src/textures/stone.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
src/textures/tiger.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/textures/water.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/textures/wood.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,105 @@
import {moduleId} from "../constants";
import DiceBox from "@3d-dice/dice-box-threejs/dist/dice-box-threejs.es"
import {getDiceSettings, setDiceSettings} from "./utils";
export class Customization extends FormApplication {
constructor() {
super(undefined);
}
override async _render(force?: boolean, options?: Application.RenderOptions<FormApplicationOptions>): Promise<any> {
return super._render(force, options).then(() => {
console.log("render")
this.renderDicePreview();
});
}
private renderDicePreview() {
let diceSettings = getDiceSettings();
let themeSelector = this.element.find("#theme");
this.themes.forEach((value, key) => {
themeSelector.append($(`<option value=${key}>${value}</option>`))
})
themeSelector.val(diceSettings.theme)
this.element.find('#faceColor').val(diceSettings.faceColor)
this.element.find('#numberColor').val(diceSettings.numberColor)
this.element.find("#preview").on("click", async () => {
this.element.find("#app").empty()
this.diceBox = new DiceBox("#app", {
assetPath: `modules/${moduleId}/`,
light_intensity: 2,
gravity_multiplier: 600,
baseScale: 120,
strength: Math.floor(Math.random() * 4),
});
await this.diceBox.initialize();
this.diceBox.clearDice();
await this.diceBox.updateConfig({
theme_customColorset: {
background: this.element.find('#faceColor').val(),
foreground: this.element.find('#numberColor').val(),
texture: this.element.find('#theme').val()
}
});
await this.diceBox.roll('5d6');
})
this.element.find("#preview").trigger("click")
this.element.find("#save").on("click", async () => {
await setDiceSettings({
theme: this.element.find('#theme').val() as string,
faceColor: this.element.find('#faceColor').val() as string,
numberColor: this.element.find('#numberColor').val() as string
})
})
}
private diceBox: typeof DiceBox
static override get defaultOptions(): FormApplicationOptions {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "dice-tower-customization",
title: "DICETOWER.customizeTitle",
template: `modules/${moduleId}/templates/dice-tower-customization.hbs`,
width: 600,
height: 620,
}) as FormApplicationOptions;
}
protected _updateObject(_event: Event, _formData: object | undefined): Promise<unknown> {
return Promise.resolve(undefined);
}
themes = new Map([
["cloudy", "Clouds (Transparent)"],
["cloudy_2", "Clouds"],
["fire", "Fire"],
["marble", "Marble"],
["water", "Water"],
["ice", "Ice"],
["paper", "Paper"],
["speckles", "Speckles"],
["glitter", "Glitter"],
["glitter_2", "Glitter (Transparent)"],
["stars", "Stars"],
["stainedglass", "Stained Glass"],
["wood", "Wood"],
["metal", "Stainless Steel"],
["skulls", "Skulls"],
["leopard", "Leopard"],
["tiger", "Tiger"],
["cheetah", "Cheetah"],
["dragon", "Dragon"],
["lizard", "Lizard"],
["bird", "Bird"],
["astral", "Astral Sea"],
["bronze01", "Bronze 1"],
["bronze02", "Bronze 2"],
["bronze03", "Bronze 3"],
["bronze03a", "Bronze 3a"],
["bronze03b", "Bronze 3b"],
["bronze04", "Bronze 4"],
["none", "none"]
]);
}

34
src/ts/apps/diceSocket.ts Normal file
View File

@ -0,0 +1,34 @@
import {moduleId} from "../constants";
import {SocketMessage} from "../types/SocketMessage";
import {SocketType} from "../types/socketType";
import {currentActor, setSetting} from "./utils";
import {DiceTower} from "../types";
export class DiceSocket {
static socketName = `module.${moduleId}`;
constructor() {
game.socket?.on(DiceSocket.socketName, this.onData)
}
private async onData(data: SocketMessage) {
switch (data.type) {
case SocketType.SETTINGS:
if (game.user?.isGM && game.actors?.get(data.payload.key)?.testUserPermission(game.users?.get(data.from) as foundry.documents.BaseUser, "OWNER")) {
setSetting(`${data.payload.key}-dice`, data.payload.value, true);
}
break;
case SocketType.RERENDER:
if (data.payload.key == currentActor() && ((game as Game).modules.get(moduleId) as unknown as DiceTower).customization.rendered) {
console.log(`rerendering ${data.payload.key}`);
await ((game as Game).modules.get(moduleId) as unknown as DiceTower).customization.close();
((game as Game).modules.get(moduleId) as unknown as DiceTower).customization.render(true);
}
}
}
emit(messaage: SocketMessage, timeout = 0) {
game.socket?.timeout(timeout).emit(DiceSocket.socketName, messaage)
}
}

62
src/ts/apps/overlays.ts Normal file
View File

@ -0,0 +1,62 @@
import {moduleId} from "../constants";
import {getSetting, overlayUrl, resultsUrl} from "./utils";
export class Overlays extends FormApplication {
constructor() {
super(undefined);
}
override async _render(force?: boolean, options?: Application.RenderOptions<FormApplicationOptions>): Promise<any> {
return super._render(force, options).then(() => {
this.renderUrls();
});
}
private renderUrls() {
let room = getSetting("diceTowerRoom") as String;
let resultOverlay = this.element.find('#result-overlay')
let diceOverlay = this.element.find('#dice-overlay')
if (game.user?.isGM) {
resultOverlay.append($(`
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline;">
<label for="myResultsId">All Results-Overlay </label>
<input type="text" readonly id="myResultsId" style="flex-grow: 1" value="${resultsUrl()}"/>
</div>
`))
}
game.users?.forEach(u => {
if (game.user?.isGM || u.id === game.userId) {
let user = u as foundry.documents.BaseUser;
diceOverlay.append($(`
<div id="${room}:${user.id}" style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline">
<label for="${room}:${user.id}url">Dice-Overlay for <strong>${user.name}</strong></label>
<input type="text" readonly style="flex-grow: 1" value="${overlayUrl(u.id)}">
</div>
`))
resultOverlay.append($(`
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline;">
<label for="myResultsId">My-Results-Overlay </label>
<input type="text" readonly id="myResultsId" style="flex-grow: 1" value="${resultsUrl(u.id)}"/>
</div>
`))
}
})
}
static override get defaultOptions(): FormApplicationOptions {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "dice-tower-overlays",
title: "DICETOWER.overlaysTitle",
template: `modules/${moduleId}/templates/dice-tower-overlays.hbs`,
width: 800,
height: 400,
}) as FormApplicationOptions;
}
protected _updateObject(_event: Event, _formData: object | undefined): Promise<unknown> {
return Promise.resolve(undefined);
}
}

34
src/ts/apps/service.ts Normal file
View File

@ -0,0 +1,34 @@
import {getDiceSettings, getSetting, overlayUrl} from "./utils";
export class Service {
register() {
if (getSetting("sendToDiceTower") as Boolean) {
let url = getSetting("diceTowerUrl") as String;
let room = getSetting("diceTowerRoom") as String;
let httpRequest = new XMLHttpRequest();
httpRequest.open('POST', url + '/dice/' + room + '/register')
httpRequest.setRequestHeader('Content-Type', 'application/json')
httpRequest.send(JSON.stringify({
name: game.user?.name,
overlay: overlayUrl(game.userId as string),
id: room + ':' + game.userId
}))
}
}
roll(value: { name: string, command: string, results: any[] }) {
this.register();
let url = getSetting("diceTowerUrl") as String;
let room = getSetting("diceTowerRoom") as String;
let payload = getDiceSettings() as any;
payload.name = value.name;
payload.command = value.command;
payload.results = value.results;
let httpRequest = new XMLHttpRequest();
httpRequest.open('POST', url + '/dice/' + room + ':' + game.userId)
httpRequest.setRequestHeader('Content-Type', 'application/json')
httpRequest.send(JSON.stringify(payload))
}
}

94
src/ts/apps/utils.ts Normal file
View File

@ -0,0 +1,94 @@
import {moduleId} from "../constants";
import {DiceTower} from "../types";
import {SocketType} from "../types/socketType";
export function currentActor() {
return ((game as Game).modules.get(moduleId) as unknown as DiceTower).controls.find('#dice-tower-actor').val()
}
export function getDiceSettings(): Dice {
let settingFor = currentActor();
if (settingFor) {
return Object.assign((getSetting("default-dice") as Dice), getSetting(`${settingFor}-dice`, "default-dice"));
} else {
return getSetting("default-dice")
}
}
export async function setDiceSettings(settings: Dice): Promise<void> {
let settingFor = currentActor();
if (settingFor) {
if (game.user?.isGM) {
await setSetting(`${settingFor}-dice`, settings, true);
ui.notifications?.info("Dice-Customization saved", {localize: true})
} else {
((game as Game).modules.get(moduleId) as unknown as DiceTower).diceSocket.emit({
type: SocketType.SETTINGS,
from: game.userId as string,
payload: {
key: settingFor,
value: settings
}
})
ui.notifications?.info("Dice-Customization save-request sent", {localize: true})
}
((game as Game).modules.get(moduleId) as unknown as DiceTower).diceSocket.emit({
type: SocketType.RERENDER,
from: game.userId as string,
payload: {
key: settingFor
}
}, 1000)
}
}
export function getSetting<D>(key: string, defaultKey?: any): D {
if (defaultKey) {
try {
getSetting(key)
} catch (_) {
registerSetting(key, getSetting(defaultKey));
}
return getSetting(key)
} else {
// @ts-ignore
return game.settings?.get(moduleId, key) as D
}
}
export async function setSetting(key: string, value: any, force = false) {
if (force) {
try {
await setSetting(key, value, false)
} catch (e) {
registerSetting(key, value);
}
} else {
// @ts-ignore
await game.settings?.set(moduleId, key, value);
}
}
export function registerSetting(key: string, value: any, options: {scope: 'client'|'world', config: boolean} = { scope: "world", config: false }) {
// @ts-ignore
game.settings?.register(moduleId, key, {
name: `DICETOWER.${key}Name`,
scope: options.scope,
default: value,
type: typeof value === 'string' ? String : typeof value === 'boolean' ? Boolean : typeof value === 'number' ? Number : Object,
config: options.config
})
}
export function overlayUrl(user: string): string {
let url = getSetting("diceTowerUrl") as String;
let room = getSetting("diceTowerRoom") as String;
return url + '/overlay/' + room + ':' + user + '?scale=10&clearAfter=30'
}
export function resultsUrl(user?: string): string {
let url = getSetting("diceTowerUrl") as String;
let room = getSetting("diceTowerRoom") as String;
return url + '/overlay/' + room + 'results' + (user ? '?user=' + user : '')
}

3
src/ts/constants.ts Normal file
View File

@ -0,0 +1,3 @@
import { id } from "../module.json";
export const moduleId = id;

1
src/ts/dice-box.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module '@3d-dice/dice-box-threejs/dist/dice-box-threejs.es'

187
src/ts/dice-tower.ts Normal file
View File

@ -0,0 +1,187 @@
import "../styles/style.scss";
import {moduleId} from "./constants";
import {DiceTower} from "./types";
import {Customization} from "./apps/customization";
import {DiceSocket} from "./apps/diceSocket";
import {getSetting, registerSetting, setSetting} from "./apps/utils";
import {Service} from "./apps/service";
import {Overlays} from "./apps/overlays";
let module: DiceTower;
Hooks.once("init", async () => {
module = (game as Game).modules.get(moduleId) as unknown as DiceTower;
module.log = (message) => console.log(`Dice-Tower | `, message)
module.log("Initialized")
module.customization = new Customization()
module.overlays = new Overlays()
module.diceSocket = new DiceSocket()
module.service = new Service()
registerSettings()
});
Hooks.on("renderChatLog", async (_: Application, _html: JQuery): Promise<void> => {
module.log("Rendering Controls")
module.controls = $(await renderTemplate(`modules/${moduleId}/templates/dice-tower-controls.hbs`, {}))
await renderDiceTowerControls(new Map([
...(new Map(
["d4", "d6", "d8", "d10", "d12", "d20", "d100"]
.map(dice => [`dice-tower-roll-${dice}`, (controls: JQuery) => {
roll(controls.find(`#dice-tower-amount`), dice)
}]))),
...(new Map([
["dice-tower-sheet", (_: JQuery) => {
openSheet()
}],
["dice-tower-customize", (_: JQuery) => {
module.customization.render(true)
}],
["dice-tower-urls", (_: JQuery) => {
module.overlays.render(true)
}],
["dice-tower-remove-dice", (controls: JQuery) => {
removeDice(controls.find("#dice-tower-amount"))
}],
["dice-tower-add-dice", (controls: JQuery) => {
addDice(controls.find("#dice-tower-amount"))
},
]]
))]
));
module.log("Render complete")
});
Hooks.once("ready", () => {
fillActors();
module.service.register()
})
Hooks.on("preCreateChatMessage", (message: ChatMessage): void => {
if (message.isRoll && getSetting("sendToDiceTower") as Boolean) {
let results = []
let command = "";
message.rolls.forEach(roll => {
let sides
let modifier = 0
let modifierMultiplikator = 1
let rolls: {value: number}[] = []
roll.terms.forEach(term => {
if (term instanceof foundry.dice.terms.Die) {
let t = term as foundry.dice.terms.Die;
sides = t.faces
let c = t.number + 'd' + sides;
if (command.length > 0) {
command = [command, c].join(" ")
} else {
command = c
}
t.results.map(it => {
return {value: it.result}
}).forEach(it => rolls.push(it))
}
if (term instanceof foundry.dice.terms.OperatorTerm ) {
let t = term as foundry.dice.terms.OperatorTerm
if (t.operator === '+') {
modifierMultiplikator = 1
} else if (t.operator === '-') {
modifierMultiplikator = -1
}
}
if (term instanceof foundry.dice.terms.NumericTerm) {
let t = term as foundry.dice.terms.NumericTerm
modifier += t.number * modifierMultiplikator
}
});
results.push({
sides: sides,
modifier: modifier,
value: roll.total,
rolls: rolls
})
module.service.roll({
name: message.alias,
command: command,
results: results
})
})
}
})
Hooks.on("updateUser", (_user, _character: {character: string}, _time, _id) => {
fillActors();
})
function registerSettings() {
game.settings?.registerMenu(moduleId, "overlays", {
name: "DICETOWER.overlaysTitle",
label: "DICETOWER.overlaysTitle",
type: Overlays,
icon: "fas fa-link"
})
setSetting("default-dice", {
theme: 'none',
faceColor: '#8d8981',
numberColor: '#000000'
}, true)
registerSetting("sendToDiceTower", false, {scope: "world", config: true})
registerSetting("diceTowerUrl", "https://dice-tower.com", {scope: "world", config: true})
registerSetting("diceTowerRoom", "", {scope: "world", config: true})
}
function openSheet() {
let actorId = module.controls.find('#dice-tower-actor').val();
if (actorId) {
let actor = game.actors?.get(actorId as string);
if (actor) {
Hotbar.toggleDocumentSheet(actor.uuid);
}
}
}
function fillActors() {
let actors = module.controls.find("#dice-tower-actor");
actors.empty()
if (game.user?.character) {
actors.append($(`<option value="${game.user?.character?.id}" selected>${game.user?.character?.name}</option>`));
} else {
actors.append($(`<option value="${game.user?.id}" selected>${game.user?.name}</option>`));
}
if (game.user?.isGM) {
let optionGroups = new Map<string, JQuery>
game.actors?.forEach(e => {
let actor = e as foundry.documents.BaseActor;
if (actor.folder) {
if (!optionGroups.has(actor.folder.uuid)) {
optionGroups.set(actor.folder.uuid, $(`<optgroup label="${actor.folder.name}"></optgroup>`));
}
optionGroups.get(actor.folder.uuid)?.append($(`<option value="${actor.id}">${actor.name}</option>`))
} else {
actors.append($(`<option value="${actor.id}">${actor.name}</option>`))
}
});
optionGroups.forEach(value => actors.append(value))
}
}
async function roll(diceAmount: JQuery, dice: string) {
new Roll(diceAmount.text() + dice).evaluate().then(roll => roll.toMessage({speaker: {actor: module.controls.find('#dice-tower-actor').val() as string}}));
}
async function renderDiceTowerControls(actions: Map<string, (controls: JQuery) => void>) {
actions.forEach((value, key) => {
module.controls.find(`#${key}`).on("click", () => value(module.controls))}
)
module.controls.insertAfter("#chat-controls")
}
function addDice(diceAmount: JQuery) {
let amount = +diceAmount.text()
diceAmount.text(`${amount + 1}`)
}
function removeDice(diceAmount: JQuery) {
let amount = +diceAmount.text()
if (amount > 1) {
diceAmount.text(`${amount - 1}`)
}
}

13
src/ts/types.ts Normal file
View File

@ -0,0 +1,13 @@
import {Customization} from "./apps/customization";
import {DiceSocket} from "./apps/diceSocket";
import {Service} from "./apps/service";
import {Overlays} from "./apps/overlays";
export interface DiceTower extends foundry.packages.BaseModule {
service: Service;
diceSocket: DiceSocket;
overlays: Overlays,
customization: Customization;
log: (message: any) => void;
controls: JQuery;
}

5
src/ts/types/Dice.ts Normal file
View File

@ -0,0 +1,5 @@
interface Dice {
theme: string
faceColor: string,
numberColor: string
}

View File

@ -0,0 +1,8 @@
import {SocketType} from "./socketType";
export interface SocketMessage {
type: SocketType,
from: string,
payload: any
}

View File

@ -0,0 +1,4 @@
export enum SocketType {
SETTINGS,
RERENDER
}

33
tsconfig.json Normal file
View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"types": [
"@league-of-foundry-developers/foundry-vtt-types",
],
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ESNext",
"DOM"
],
"moduleResolution": "Node",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": false,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noImplicitOverride": true,
"noImplicitAny": true,
"skipLibCheck": true,
"allowJs": true,
"checkJs": false,
},
"include": [
"src",
"src/**/*.js"
]
}

76
vite.config.ts Normal file
View File

@ -0,0 +1,76 @@
import * as fsPromises from "fs/promises";
import copy from "rollup-plugin-copy";
import scss from "rollup-plugin-scss";
import {defineConfig, Plugin} from "vite";
const moduleVersion = process.env.MODULE_VERSION;
const githubProject = process.env.GH_PROJECT;
const githubTag = process.env.GH_TAG;
export default defineConfig({
build: {
sourcemap: true,
rollupOptions: {
input: "src/ts/dice-tower.ts",
output: {
entryFileNames: "scripts/[name].js",
dir: "dist",
format: "es",
assetFileNames: "[name][extname]"
},
},
},
plugins: [
scss({
name: "styles/style.css",
sourceMap: true,
watch: ["src/styles/*.scss"],
}),
copy({
targets: [
{ src: "src/languages", dest: "dist" },
{ src: "src/templates", dest: "dist" },
{ src: "src/libs", dest: "dist" },
{ src: "src/textures", dest: "dist" },
],
hook: "writeBundle",
}),
updateModuleManifestPlugin(),
],
});
function updateModuleManifestPlugin(): Plugin {
return {
name: "update-module-manifest",
async writeBundle(): Promise<void> {
const packageContents = JSON.parse(
await fsPromises.readFile("./package.json", "utf-8")
) as Record<string, unknown>;
const version = moduleVersion || (packageContents.version as string);
const description = (packageContents.description as string);
const manifestContents: string = await fsPromises.readFile(
"src/module.json",
"utf-8"
);
const manifestJson = JSON.parse(manifestContents) as Record<
string,
unknown
>;
manifestJson["version"] = version;
manifestJson["description"] = description;
if (githubProject) {
const baseUrl = `https://github.com/${githubProject}/releases`;
manifestJson["manifest"] = `${baseUrl}/latest/download/module.json`;
if (githubTag) {
manifestJson[
"download"
] = `${baseUrl}/download/${githubTag}/module.zip`;
}
}
await fsPromises.writeFile(
"dist/module.json",
JSON.stringify(manifestJson, null, 4)
);
},
};
}