Implement the most basic version of a dice pool configuration

This commit is contained in:
Oliver-Akins 2025-01-18 15:59:15 -07:00
parent c62d3cae2f
commit a95412ad2e
19 changed files with 465 additions and 30 deletions

View file

@ -5,4 +5,7 @@ Soetarman Atmodjo:
- icons/roll.svg (https://thenounproject.com/icon/dice-5195278/) : Rights Purchased. - icons/roll.svg (https://thenounproject.com/icon/dice-5195278/) : Rights Purchased.
SuperNdre: SuperNdre:
- icons/edit.svg (https://thenounproject.com/icon/edit-5208207/) : Rights Purchased - icons/edit.svg (https://thenounproject.com/icon/edit-5208207/) : Rights Purchased
YANDI RS:
- icons/d8-outline (https://thenounproject.com/icon/d8-7272826/) : Rights Purchased

View file

@ -0,0 +1,3 @@
<svg version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<path d="m51.176 1.332 20.191 11.656 0.11719 0.074219 20.094 11.602c0.73438 0.42188 1.1445 1.1914 1.1484 1.9844h0.007813v46.699c0 0.92578-0.55078 1.7266-1.3438 2.0898l-20.027 11.562 0.003907 0.007812-20.223 11.672c-0.74219 0.43359-1.6328 0.39453-2.3203-0.015626l-20.191-11.656-0.11719-0.074218-20.094-11.602c-0.73438-0.42188-1.1445-1.1914-1.1484-1.9844h-0.007813v-46.699c0-0.92578 0.55078-1.7266 1.3438-2.0898l20.027-11.562-0.003907-0.007813 20.223-11.672c0.74219-0.43359 1.6328-0.39453 2.3203 0.015625zm4.4375 7.8516 16.27 28.18 0.003907-0.003906 16.262 28.168v-37.562l-18.945-10.938-0.12891-0.070313zm26.922 66.09h-65.066l13.332 7.6953 0.12891 0.070312 19.074 11.012 19.074-11.012 0.003906 0.007813 13.457-7.7695zm-70.676-9.7461 16.262-28.168 0.070312-0.12109 16.199-28.055-13.461 7.7734-0.003907-0.007812-19.07 11.012v37.562zm56.066-25.879-17.918-31.035-17.852 30.918-0.070312 0.12891-17.914 31.027h71.66l-17.914-31.027 0.003906-0.003906-0.003906-0.007812z"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -90,7 +90,18 @@
"long-range": "Long @RipCrypt.common.range", "long-range": "Long @RipCrypt.common.range",
"current-wear": "Current @RipCrypt.common.wear", "current-wear": "Current @RipCrypt.common.wear",
"max-wear": "Maximum @RipCrypt.common.wear", "max-wear": "Maximum @RipCrypt.common.wear",
"location-placeholder": "New Location..." "location-placeholder": "New Location...",
"numberOfDice": "# of Dice",
"rollTarget": "Target",
"difficulty": "(DC: {dc})"
},
"notifs": {
"error": {
"invalid-delta": "The delta for \"{name}\" is not a number, cannot finish processing the action."
},
"warn": {
"cannot-go-negative": "\"{name}\" is unable to be a negative number."
}
} }
} }
} }

158
module/Apps/DicePool.mjs Normal file
View file

@ -0,0 +1,158 @@
import { filePath } from "../consts.mjs";
import { GenericAppMixin } from "./GenericApp.mjs";
import { localizer } from "../utils/Localizer.mjs";
import { Logger } from "../utils/Logger.mjs";
const { Roll } = foundry.dice;
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export class DicePool extends GenericAppMixin(HandlebarsApplicationMixin(ApplicationV2)) {
// #region Options
static DEFAULT_OPTIONS = {
classes: [
`ripcrypt--DicePool`,
],
window: {
title: `Dice Pool`,
frame: true,
positioned: true,
resizable: false,
minimizable: true,
},
position: {
width: `auto`,
height: `auto`,
},
actions: {
diceCountDelta: this.#diceCountDelta,
targetDelta: this.#targetDelta,
roll: this.#roll,
},
};
static PARTS = {
numberOfDice: {
template: filePath(`templates/Apps/DicePool/numberOfDice.hbs`),
},
target: {
template: filePath(`templates/Apps/DicePool/target.hbs`),
},
buttons: {
template: filePath(`templates/Apps/DicePool/buttons.hbs`),
},
};
// #endregion
// #region Instance Data
_diceCount;
_target;
constructor({
diceCount = 1,
target,
flavor = ``,
...opts
} = {}) {
super(opts);
this._flavor = flavor;
this._diceCount = diceCount;
this._target = target ?? game.settings.get(`ripcrypt`, `dc`) ?? 1;
};
get title() {
if (!this._flavor) {
return super.title;
}
return `${super.title}: ${this._flavor}`;
};
// #endregion
// #region Lifecycle
async _preparePartContext(partId, ctx, _opts) {
ctx = {};
switch (partId) {
case `numberOfDice`: {
this._prepareNumberOfDice(ctx);
break;
};
case `target`: {
this._prepareTarget(ctx);
break;
};
case `buttons`: {
break;
};
}
Logger.debug(`${partId} Context:`, ctx);
return ctx;
};
async _prepareNumberOfDice(ctx) {
ctx.numberOfDice = this._diceCount;
ctx.decrementDisabled = this._diceCount <= 0;
};
async _prepareTarget(ctx) {
ctx.target = this._target;
ctx.incrementDisabled = this._target >= 8;
ctx.decrementDisabled = this._target <= 1;
};
// #endregion
// #region Actions
static async #diceCountDelta(_event, element) {
const delta = parseInt(element.dataset.delta);
if (Number.isNaN(delta)) {
ui.notifications.error(
localizer(`RipCrypt.notifs.error.invalid-delta`, { name: `@RipCrypt.Apps.numberOfDice` }),
);
return;
};
let newCount = this._diceCount + delta;
if (newCount < 0) {
ui.notifications.warn(
localizer(`RipCrypt.notifs.warn.cannot-go-negative`, { name: `@RipCrypt.Apps.numberOfDice` }),
);
};
this._diceCount = Math.max(newCount, 0);
this.render({ parts: [`numberOfDice`] });
};
static async #targetDelta(_event, element) {
const delta = parseInt(element.dataset.delta);
if (Number.isNaN(delta)) {
ui.notifications.error(
localizer(`RipCrypt.notifs.error.invalid-delta`, { name: `@RipCrypt.Apps.rollTarget` }),
);
return;
};
this._target += delta;
this.render({ parts: [`target`] });
};
static async #roll() {
const formula = `${this._diceCount}d8rc${this._target}`;
Logger.debug(`Attempting to roll formula: ${formula}`);
let flavor = this._flavor;
if (this._flavor) {
flavor += ` ` + localizer(`RipCrypt.Apps.difficulty`, { dc: this._target });
}
const roll = new Roll(formula);
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor,
});
this.close();
};
// #endregion
};

View file

@ -1,7 +1,4 @@
import { localizer } from "../utils/Localizer.mjs"; import { DicePool } from "./DicePool.mjs";
import { Logger } from "../utils/Logger.mjs";
const { Roll } = foundry.dice;
/** /**
* A mixin that takes the class from HandlebarsApplicationMixin and * A mixin that takes the class from HandlebarsApplicationMixin and
@ -46,20 +43,11 @@ export function GenericAppMixin(HandlebarsApp) {
/** @this {GenericRipCryptApp} */ /** @this {GenericRipCryptApp} */
static async rollDice(_$e, el) { static async rollDice(_$e, el) {
const data = el.dataset; const data = el.dataset;
const formula = data.formula; const diceCount = parseInt(data.diceCount);
Logger.debug(`Attempting to roll formula: ${formula}`); const flavor = data.flavor;
let flavor; const dp = new DicePool({ diceCount, flavor });
if (data.flavor) { dp.render({ force: true });
flavor = localizer(data.flavor);
}
const roll = new Roll(formula);
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor,
});
}; };
// #endregion // #endregion
}; };

View file

@ -0,0 +1,55 @@
import { StyledShadowElement } from "./mixins/StyledShadowElement.mjs";
/**
Attributes:
*/
export class RipCryptBorder extends StyledShadowElement(HTMLElement) {
static elementName = `rc-border`;
static formAssociated = false;
/* Stuff for the mixin to use */
static _stylePath = `css/components/rc-border.css`;
#container;
_mounted = false;
async connectedCallback() {
super.connectedCallback();
if (this._mounted) { return };
/*
This converts all of the double-dash prefixed properties on the element to
CSS variables so that they don't all need to be provided by doing style=""
*/
for (const attrVar of this.attributes) {
if (attrVar.name?.startsWith(`var:`)) {
const prop = attrVar.name.replace(`var:`, ``);
this.style.setProperty(`--` + prop, attrVar.value);
};
};
this.#container = document.createElement(`div`);
this.#container.classList = `rc-border`;
const titleContainer = document.createElement(`div`);
titleContainer.classList = `title`;
const titleSlot = document.createElement(`slot`);
titleSlot.innerHTML = `No Title`;
titleSlot.name = `title`;
titleContainer.appendChild(titleSlot.cloneNode(true));
this.#container.appendChild(titleContainer.cloneNode(true));
const contentSlot = document.createElement(`slot`);
contentSlot.name = `content`;
this.#container.appendChild(contentSlot.cloneNode(true));
this._shadow.appendChild(this.#container);
this._mounted = true;
};
disconnectedCallback() {
super.disconnectedCallback();
if (!this._mounted) { return };
this._mounted = false;
};
};

View file

@ -1,10 +1,12 @@
import { Logger } from "../../utils/Logger.mjs"; import { Logger } from "../../utils/Logger.mjs";
import { RipCryptBorder } from "./RipCryptBorder.mjs";
import { RipCryptIcon } from "./Icon.mjs"; import { RipCryptIcon } from "./Icon.mjs";
import { RipCryptSVGLoader } from "./svgLoader.mjs"; import { RipCryptSVGLoader } from "./svgLoader.mjs";
const components = [ const components = [
RipCryptIcon, RipCryptIcon,
RipCryptSVGLoader, RipCryptSVGLoader,
RipCryptBorder,
]; ];
export function registerCustomComponents() { export function registerCustomComponents() {

22
module/api.mjs Normal file
View file

@ -0,0 +1,22 @@
import { CombinedHeroSheet } from "./Apps/ActorSheets/CombinedHeroSheet.mjs";
import { DicePool } from "./Apps/DicePool.mjs";
import { HeroSkillsCardV1 } from "./Apps/ActorSheets/HeroSkillsCardV1.mjs";
import { HeroSummaryCardV1 } from "./Apps/ActorSheets/HeroSummaryCardV1.mjs";
const { deepFreeze } = foundry.utils;
Object.defineProperty(
globalThis,
`ripcrypt`,
{
value: deepFreeze({
Apps: {
DicePool,
CombinedHeroSheet,
HeroSummaryCardV1,
HeroSkillsCardV1,
},
}),
writable: false,
},
);

View file

@ -2,3 +2,6 @@
import "./hooks/init.mjs"; import "./hooks/init.mjs";
import "./hooks/ready.mjs"; import "./hooks/ready.mjs";
import "./hooks/hotReload.mjs"; import "./hooks/hotReload.mjs";
// Global API
import "./api.mjs";

View file

@ -0,0 +1,8 @@
export function registerMetaSettings() {
game.settings.register(`ripcrypt`, `dc`, {
scope: `world`,
type: Number,
config: false,
requiresReload: false,
});
};

View file

@ -0,0 +1,8 @@
<div class="button-row">
<button
type="button"
data-action="roll"
>
Roll
</button>
</div>

View file

@ -0,0 +1,33 @@
<rc-border
var:border-color="var(--accent-1)"
var:padding-top="16px"
>
<div slot="title">
{{ rc-i18n "RipCrypt.Apps.numberOfDice" }}
</div>
<div slot="content" class="d8-incrementer">
<button
type="button"
data-action="diceCountDelta"
data-delta="1"
>
+
</button>
<div class="dice-count-row">
<rc-icon
name="icons/d8-outline"
var:size="35px"
var:fill="var(--accent-1)"
></rc-icon>
<span><span aria-hidden="true">x</span>{{ numberOfDice }}</span>
</div>
<button
type="button"
data-action="diceCountDelta"
data-delta="-1"
{{#if decrementDisabled}}disabled{{/if}}
>
-
</button>
</div>
</rc-border>

View file

@ -0,0 +1,68 @@
.ripcrypt.ripcrypt--DicePool {
> .window-content {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: auto min-content;
background: var(--base-background);
padding: 8px;
gap: 8px;
width: 250px;
height: 185px;
}
--button-background: var(--alt-row-background);
--button-text: var(--alt-row-text);
button {
border-radius: 2px;
font-weight: bold;
padding: 4px 8px;
width: max-content;
}
.d8-incrementer {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
button {
width: max-content;
}
}
.dice-count-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
gap: 4px;
font-size: 1.5rem;
}
.dice-target {
display: flex;
position: relative;
.value {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
color: white;
font-weight: normal;
}
}
.button-row {
--button-background: var(--header-background);
--button-text: var(--header-text);
display: flex;
justify-content: center;
align-items: center;
grid-column: 1 / -1;
}
}

View file

@ -0,0 +1,32 @@
<rc-border
var:border-color="var(--accent-1)"
var:padding-top="16px"
>
<div slot="title">Target</div>
<div slot="content" class="d8-incrementer">
<button
type="button"
data-action="targetDelta"
data-delta="1"
{{#if incrementDisabled}}disabled{{/if}}
>
+
</button>
<div class="dice-target">
<rc-icon
name="icons/d8-outline"
var:size="35px"
var:fill="var(--accent-1)"
></rc-icon>
<div class="value">{{target}}</div>
</div>
<button
type="button"
data-action="targetDelta"
data-delta="-1"
{{#if decrementDisabled}}disabled{{/if}}
>
-
</button>
</div>
</rc-border>

View file

@ -261,8 +261,8 @@
type="button" type="button"
class="roll icon" class="roll icon"
data-action="roll" data-action="roll"
data-formula="{{ability.value}}d8rc4" data-dice-count="{{ability.value}}"
data-flavor="{{ability.name}} Roll (Difficulty: 4)" data-flavor="{{ability.name}} Roll"
> >
<rc-icon <rc-icon
var:size="20px" var:size="20px"

View file

@ -1,4 +1,5 @@
@import url("./AllItemSheetV1/style.css"); @import url("./AllItemSheetV1/style.css");
@import url("./CombinedHeroSheet/style.css"); @import url("./CombinedHeroSheet/style.css");
@import url("./DicePool/style.css");
@import url("./HeroSummaryCardV1/style.css"); @import url("./HeroSummaryCardV1/style.css");
@import url("./HeroSkillsCardV1/style.css"); @import url("./HeroSkillsCardV1/style.css");

View file

@ -11,6 +11,7 @@
.ripcrypt { .ripcrypt {
.window-content { .window-content {
flex: initial;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }

View file

@ -0,0 +1,37 @@
:host {
display: flex;
flex-direction: column;
--vertical-displacement: 10px;
}
.rc-border {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-grow: 1;
padding: var(--padding, 4px);
border-width: 2px;
border-style: solid;
border-color: var(--border-color);
border-radius: 8px;
position: relative;
margin-top: var(--margin-top, var(--vertical-displacement));
padding-top: var(--padding-top, var(--vertical-displacement));
.title {
position: absolute;
top: calc(-1 * var(--vertical-displacement));
left: 50%;
height: minmax(var(--title-height, 20px), auto);
transform: translateX(-50%);
background: var(--title-background, var(--border-color));
padding: 4px 6px;
box-sizing: border-box;
width: max-content;
max-width: 75%;
min-width: 50px;
border-radius: 4px;
}
}

View file

@ -1,9 +1,20 @@
.ripcrypt > .window-content button { .ripcrypt > .window-content button {
all: revert; all: revert;
outline: none;
border: none;
padding: 2px 4px; padding: 2px 4px;
background: var(--button-background); background: var(--button-background);
color: var(--button-text); color: var(--button-text);
&:hover:not(:disabled) {
cursor: pointer;
outline: none;
}
&:disabled {
opacity: 0.6;
}
&.icon { &.icon {
padding: 0; padding: 0;
border-radius: 50%; border-radius: 50%;
@ -11,14 +22,5 @@
border: none; border: none;
width: 20px; width: 20px;
height: 20px; height: 20px;
&:hover:not(:disabled) {
cursor: pointer;
outline: none;
}
&:disabled {
opacity: 0.6;
}
} }
} }