Initial Commit
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
yarn.lock linguist-generated
|
1
.gitignore
vendored
@ -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
22
package.json
Normal 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
@ -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
@ -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
@ -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;
|
||||
}
|
31
src/templates/dice-tower-controls.hbs
Normal 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>`
|
20
src/templates/dice-tower-customization.hbs
Normal 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>`
|
5
src/templates/dice-tower-overlays.hbs
Normal 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
After Width: | Height: | Size: 2.1 KiB |
BIN
src/textures/bronze01.webp
Executable file
After Width: | Height: | Size: 197 KiB |
BIN
src/textures/bronze02.webp
Executable file
After Width: | Height: | Size: 165 KiB |
BIN
src/textures/bronze03.webp
Executable file
After Width: | Height: | Size: 190 KiB |
BIN
src/textures/bronze03a.webp
Executable file
After Width: | Height: | Size: 182 KiB |
BIN
src/textures/bronze03b.webp
Executable file
After Width: | Height: | Size: 140 KiB |
BIN
src/textures/bronze04.webp
Executable file
After Width: | Height: | Size: 184 KiB |
BIN
src/textures/cheetah.webp
Executable file
After Width: | Height: | Size: 21 KiB |
BIN
src/textures/cloudy.alt.webp
Executable file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/textures/cloudy.webp
Executable file
After Width: | Height: | Size: 28 KiB |
BIN
src/textures/dragon-bump.webp
Executable file
After Width: | Height: | Size: 6.7 KiB |
BIN
src/textures/dragon.webp
Executable file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/textures/feather-bump.webp
Executable file
After Width: | Height: | Size: 11 KiB |
BIN
src/textures/feather.webp
Executable file
After Width: | Height: | Size: 34 KiB |
BIN
src/textures/fire.webp
Executable file
After Width: | Height: | Size: 12 KiB |
BIN
src/textures/glitter-alpha.webp
Executable file
After Width: | Height: | Size: 14 KiB |
BIN
src/textures/glitter-bump.webp
Executable file
After Width: | Height: | Size: 8.3 KiB |
BIN
src/textures/glitter.webp
Executable file
After Width: | Height: | Size: 10 KiB |
BIN
src/textures/ice.webp
Executable file
After Width: | Height: | Size: 49 KiB |
BIN
src/textures/leopard.webp
Executable file
After Width: | Height: | Size: 22 KiB |
BIN
src/textures/lizard-bump.webp
Executable file
After Width: | Height: | Size: 19 KiB |
BIN
src/textures/lizard.webp
Executable file
After Width: | Height: | Size: 19 KiB |
BIN
src/textures/marble.webp
Executable file
After Width: | Height: | Size: 51 KiB |
BIN
src/textures/metal-bump.webp
Executable file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/textures/metal.webp
Executable file
After Width: | Height: | Size: 5.0 KiB |
BIN
src/textures/noise-thin-film.webp
Executable file
After Width: | Height: | Size: 16 KiB |
BIN
src/textures/noise.webp
Executable file
After Width: | Height: | Size: 33 KiB |
BIN
src/textures/paper-bump.webp
Executable file
After Width: | Height: | Size: 20 KiB |
BIN
src/textures/paper.webp
Executable file
After Width: | Height: | Size: 12 KiB |
BIN
src/textures/skulls.webp
Executable file
After Width: | Height: | Size: 7.5 KiB |
BIN
src/textures/speckles.webp
Executable file
After Width: | Height: | Size: 9.3 KiB |
BIN
src/textures/stainedglass-bump.webp
Executable file
After Width: | Height: | Size: 8.8 KiB |
BIN
src/textures/stainedglass.webp
Executable file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/textures/stars.webp
Executable file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/textures/stone.webp
Executable file
After Width: | Height: | Size: 9.5 KiB |
BIN
src/textures/tiger.webp
Executable file
After Width: | Height: | Size: 12 KiB |
BIN
src/textures/water.webp
Executable file
After Width: | Height: | Size: 20 KiB |
BIN
src/textures/wood.webp
Executable file
After Width: | Height: | Size: 16 KiB |
105
src/ts/apps/customization.ts
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,3 @@
|
||||
import { id } from "../module.json";
|
||||
|
||||
export const moduleId = id;
|
1
src/ts/dice-box.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module '@3d-dice/dice-box-threejs/dist/dice-box-threejs.es'
|
187
src/ts/dice-tower.ts
Normal 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
@ -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
@ -0,0 +1,5 @@
|
||||
interface Dice {
|
||||
theme: string
|
||||
faceColor: string,
|
||||
numberColor: string
|
||||
}
|
8
src/ts/types/SocketMessage.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {SocketType} from "./socketType";
|
||||
|
||||
export interface SocketMessage {
|
||||
type: SocketType,
|
||||
from: string,
|
||||
payload: any
|
||||
}
|
||||
|
4
src/ts/types/socketType.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum SocketType {
|
||||
SETTINGS,
|
||||
RERENDER
|
||||
}
|
33
tsconfig.json
Normal 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
@ -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)
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|