RC-51 | Roll Abilities

This commit is contained in:
Oliver-Akins 2024-12-29 21:28:13 -07:00
parent 23cad8fcc3
commit 0319841a01
14 changed files with 327 additions and 4 deletions

View file

@ -76,6 +76,20 @@
{{else}} {{else}}
<span>{{ability.value}}</span> <span>{{ability.value}}</span>
{{/unless}} {{/unless}}
{{#if @root.meta.editable}}
<button
type="button"
class="roll"
data-action="roll"
data-formula="{{ability.value}}d8rc4"
data-flavor="{{ability.name}} Roll (Difficulty: 4)"
>
<rc-icon
var:size="20px"
name="icons/roll-2"
></rc-icon>
</button>
{{/if}}
</div> </div>
{{#unless ability.readonly}} {{#unless ability.readonly}}
<label <label

View file

@ -89,6 +89,8 @@
grid-template-rows: minmax(0, 3fr) minmax(0, 1fr); grid-template-rows: minmax(0, 3fr) minmax(0, 1fr);
justify-items: center; justify-items: center;
align-items: center; align-items: center;
position: relative;
label, .label { label, .label {
width: 100%; width: 100%;
text-align: center; text-align: center;
@ -106,6 +108,7 @@
border: 2px solid black; border: 2px solid black;
border-radius: 50%; border-radius: 50%;
font-size: 1.5rem; font-size: 1.5rem;
position: relative;
> .value { > .value {
background: none; background: none;
@ -113,8 +116,15 @@
text-align: center; text-align: center;
} }
> .roll {
--distance: -15%;
position: absolute;
top: var(--distance);
right: var(--distance);
z-index: 2;
}
&.dual { &.dual {
position: relative;
font-size: var(--font-size-14); font-size: var(--font-size-14);
--distance-from-edge: 4px; --distance-from-edge: 4px;

View file

@ -1,3 +1,5 @@
@import url("./elements/button.css");
.ripcrypt { .ripcrypt {
.window-content { .window-content {
padding: 0; padding: 0;

18
Apps/components/icon.css Normal file
View file

@ -0,0 +1,18 @@
:host {
display: inline-block;
}
div {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
svg {
width: var(--size, 1rem);
height: var(--size, 1rem);
fill: var(--fill);
stroke: var(--stroke);
}

22
Apps/elements/button.css Normal file
View file

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

5
assets/README.md Normal file
View file

@ -0,0 +1,5 @@
Some of the assets provided in this repository may be Creative Commons by Attribution 3, or they may have some other license.
Oliver Akins does not grant any permission to use these assets outside of what the licenses allow. Make sure that anything you do with these assets is within the permitted usage of their respective licenses if they are outside of this repository.
For a detailed overview of what icons have what licenses, please see [the credit file](_credit.txt).

2
assets/_credit.txt Normal file
View file

@ -0,0 +1,2 @@
Soetarman Atmodjo:
- icons/roll.svg : Rights Purchased.

6
assets/icons/roll.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="83" height="95" viewBox="0 0 83 95" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g>
<title>Layer 1</title>
<path id="svg_1" d="m77.95688,69.37862c0,-1.375 1.1133,-2.4883 2.4883,-2.4883s2.4883,1.1133 2.4883,2.4883l0,0.53516c0,1.375 -1.1133,2.4883 -2.4883,2.4883s-2.4883,-1.1133 -2.4883,-2.4883l0,-0.53516zm-49.355,-34.766c1.4609,-1.4648 3.832,-1.4648 5.2969,0c1.4609,1.4609 1.4609,3.832 0,5.2969c-1.4648,1.4609 -3.832,1.4609 -5.2969,0c-1.4648,-1.4648 -1.4648,-3.832 0,-5.2969zm20.453,20.453c1.4648,-1.4609 3.832,-1.4609 5.2969,0c1.4648,1.4648 1.4648,3.832 0,5.2969c-1.4609,1.4648 -3.832,1.4648 -5.2969,0c-1.4609,-1.4609 -1.4609,-3.832 0,-5.2969zm-10.227,-10.227c1.4609,-1.4648 3.832,-1.4648 5.293,0c1.4648,1.4609 1.4648,3.832 0,5.293c-1.4609,1.4648 -3.832,1.4648 -5.293,0c-1.4648,-1.4609 -1.4648,-3.832 0,-5.293zm-9.8594,-20.359l25.016,0c2.8828,0 5.5039,1.1797 7.4062,3.082l0.00781,0.00781c1.9023,1.9023 3.082,4.5273 3.082,7.4062l0,25.016c0,2.8828 -1.1797,5.5039 -3.082,7.4062l-0.00781,0.00782c-1.9023,1.90229 -4.5273,3.08199 -7.4062,3.08199l-25.016,0c-2.8828,0 -5.5039,-1.1797 -7.4062,-3.08199l-0.00781,-0.00782c-1.9023,-1.9023 -3.082,-4.5273 -3.082,-7.4062l0,-25.016c0,-2.8828 1.1797,-5.5039 3.082,-7.4062l0.00781,-0.00781c1.9023,-1.9023 4.5273,-3.082 7.4062,-3.082zm25.016,5l-25.016,0c-1.5156,0 -2.8906,0.61719 -3.8867,1.6094c-0.99219,0.99609 -1.6094,2.375 -1.6094,3.8867l0,25.016c0,1.5156 0.61719,2.8906 1.6094,3.8867c0.99609,0.99219 2.375,1.6094 3.8867,1.6094l25.016,0c1.5156,0 2.8906,-0.61719 3.8867,-1.6094c0.99219,-0.99609 1.6094,-2.375 1.6094,-3.8867l0,-25.016c0,-1.5156 -0.61719,-2.8906 -1.6094,-3.8867c-0.99609,-0.99219 -2.375,-1.6094 -3.8867,-1.6094zm18.355,42.309c1.1914,-0.69141 2.7188,-0.28516 3.4062,0.91016c0.69141,1.1914 0.28516,2.7188 -0.91016,3.4062l-32.113,18.539c-0.84766,0.49219 -1.8672,0.42578 -2.6328,-0.08203l-38.828,-22.418c-0.80078,-0.46094 -1.25,-1.2969 -1.25,-2.1562l-0.01172,-45c0,-1.0117 0.60156,-1.8867 1.4688,-2.2773l38.766,-22.379c0.80859,-0.46484 1.7695,-0.42578 2.5195,0.02344l38.934,22.477c0.80078,0.46094 1.25,1.2969 1.25,2.1562l0,36.395c0,1.375 -1.1133,2.4883 -2.4883,2.4883s-2.4883,-1.1133 -2.4883,-2.4883l0,-34.961l-36.48,-21.062l-36.473,21.059l0,42.141l36.469,21.055l30.867,-17.82l-0.00522,-0.00647z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -5,6 +5,7 @@ import { Logger } from "../../utils/Logger.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ActorSheetV2 } = foundry.applications.sheets; const { ActorSheetV2 } = foundry.applications.sheets;
const { Roll } = foundry.dice;
export class HeroSummaryCardV1 extends HandlebarsApplicationMixin(ActorSheetV2) { export class HeroSummaryCardV1 extends HandlebarsApplicationMixin(ActorSheetV2) {
@ -23,7 +24,9 @@ export class HeroSummaryCardV1 extends HandlebarsApplicationMixin(ActorSheetV2)
window: { window: {
resizable: false, resizable: false,
}, },
actions: {}, actions: {
roll: this.rollDice,
},
form: { form: {
submitOnChange: true, submitOnChange: true,
closeOnSubmit: false, closeOnSubmit: false,
@ -131,5 +134,24 @@ export class HeroSummaryCardV1 extends HandlebarsApplicationMixin(ActorSheetV2)
// #endregion // #endregion
// #region Actions // #region Actions
static async rollDice(_$e, el) {
const data = el.dataset;
const formula = data.formula;
Logger.debug(`Attempting to roll formula: ${formula}`);
let flavor;
if (data.flavor) {
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,126 @@
import { Logger } from "../../utils/Logger.mjs";
import { StyledShadowElement } from "./mixins/StyledShadowElement.mjs";
/**
Attributes:
@property {string} name - The name of the icon, takes precedence over the path
@property {string} path - The path of the icon file
*/
export class RipCryptIcon extends StyledShadowElement(HTMLElement) {
static elementName = `rc-icon`;
static formAssociated = false;
/* Stuff for the mixin to use */
static _stylePath = `components/icon.css`;
static _cache = new Map();
#container;
/** @type {null | string} */
_name;
/** @type {null | string} */
_path;
/* Stored IDs for all of the hooks that are in this component */
#svgHmr;
constructor() {
super();
// this._shadow = this.attachShadow({ mode: `open`, delegatesFocus: true });
this.#container = document.createElement(`div`);
this._shadow.appendChild(this.#container);
};
_mounted = false;
async connectedCallback() {
super.connectedCallback();
if (this._mounted) { return };
this._name = this.getAttribute(`name`);
this._path = this.getAttribute(`path`);
/*
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);
};
};
/*
Try to retrieve the icon if it isn't present, try the path then default to
the slot content, as then we can have a default per-icon usage
*/
let content;
if (this._name) {
content = await this.#getIcon(`./systems/${game.system.id}/assets/${this._name}.svg`);
};
if (this._path && !content) {
content = await this.#getIcon(this._path);
};
if (content) {
this.#container.appendChild(content.cloneNode(true));
};
/*
This is so that when we get an HMR event from Foundry we can appropriately
handle it using our logic to update the component and the icon cache.
*/
if (game.settings.get(`ripcrypt`, `devMode`)) {
this.#svgHmr = Hooks.on(`${game.system.id}-hmr:svg`, (iconName, data) => {
if (this._name === iconName || this._path?.endsWith(data.path)) {
const svg = this.#parseSVG(data.content);
this.constructor._cache.set(iconName, svg);
this.#container.replaceChildren(svg.cloneNode(true));
};
});
};
this._mounted = true;
};
disconnectedCallback() {
super.disconnectedCallback();
if (!this._mounted) { return };
Hooks.off(`${game.system.id}-hmr:svg`, this.#svgHmr);
this._mounted = false;
};
async #getIcon(path) {
// Cache hit!
if (this.constructor._cache.has(path)) {
Logger.debug(`Icon ${path} cache hit`);
return this.constructor._cache.get(path);
};
const r = await fetch(path);
switch (r.status) {
case 200:
case 201:
break;
default:
Logger.error(`Failed to fetch icon: ${path}`);
return;
};
Logger.debug(`Adding icon ${path} to the cache`);
const svg = this.#parseSVG(await r.text());
this.constructor._cache.set(path, svg);
return svg;
};
/** Takes an SVG string and returns it as a DOM node */
#parseSVG(content) {
const temp = document.createElement(`div`);
temp.innerHTML = content;
return temp.querySelector(`svg`);
};
};

View file

@ -0,0 +1,22 @@
import { Logger } from "../../utils/Logger.mjs";
import { RipCryptIcon } from "./Icon.mjs";
const components = [
RipCryptIcon,
];
export function registerCustomComponents() {
(CONFIG.CACHE ??= {}).componentListeners ??= [];
for (const component of components) {
if (!window.customElements.get(component.elementName)) {
Logger.debug(`Registering component "${component.elementName}"`);
window.customElements.define(
component.elementName,
component,
);
if (component.formAssociated) {
CONFIG.CACHE.componentListeners.push(component.elementName);
}
};
}
};

View file

@ -0,0 +1,72 @@
/**
* @param {HTMLElement} Base
*/
export function StyledShadowElement(Base) {
return class extends Base {
/**
* The path to the CSS that is loaded
* @type {string}
*/
static _stylePath;
/**
* The stringified CSS to use
* @type {string}
*/
static _styles;
/**
* The HTML element of the stylesheet
* @type {HTMLStyleElement}
*/
_style;
/** @type {ShadowRoot} */
_shadow;
/**
* The hook ID for this element's CSS hot reload
* @type {number}
*/
#cssHmr;
constructor() {
super();
this._shadow = this.attachShadow({ mode: `open` });
this._style = document.createElement(`style`);
this._shadow.appendChild(this._style);
};
#mounted = false;
connectedCallback() {
if (this.#mounted) { return };
this._getStyles();
this.#mounted = true;
};
disconnectedCallback() {
if (!this.#mounted) { return };
if (this.#cssHmr != null) {
Hooks.off(`dd-hmr:css`, this.#cssHmr);
this.#cssHmr = null;
};
this.#mounted = false;
};
_getStyles() {
if (this.constructor._styles) {
this._style.innerHTML = this.constructor._styles;
} else {
fetch(`./systems/${game.system.id}/Apps/${this.constructor._stylePath}`)
.then(r => r.text())
.then(t => {
this.constructor._styles = t;
this._style.innerHTML = t;
});
}
};
};
};

View file

@ -11,6 +11,7 @@ import { CryptDie } from "../dice/CryptDie.mjs";
// Misc // Misc
import helpers from "../handlebarHelpers/_index.mjs"; import helpers from "../handlebarHelpers/_index.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { registerCustomComponents } from "../Apps/elements/_index.mjs";
import { registerDevSettings } from "../settings/devSettings.mjs"; import { registerDevSettings } from "../settings/devSettings.mjs";
import { registerUserSettings } from "../settings/userSettings.mjs"; import { registerUserSettings } from "../settings/userSettings.mjs";
@ -44,5 +45,6 @@ Hooks.once(`init`, () => {
// #region Token Attrs // #region Token Attrs
CONFIG.Actor.trackableAttributes.hero = HeroData.trackableAttributes; CONFIG.Actor.trackableAttributes.hero = HeroData.trackableAttributes;
registerCustomComponents();
Handlebars.registerHelper(helpers); Handlebars.registerHelper(helpers);
}); });

View file

@ -36,7 +36,7 @@
"flags": { "flags": {
"hotReload": { "hotReload": {
"extensions": ["css", "hbs", "json", "mjs", "svg"], "extensions": ["css", "hbs", "json", "mjs", "svg"],
"paths": ["Apps", "langs", "module"] "paths": ["assets", "Apps", "langs", "module"]
} }
}, },
"documentTypes": { "documentTypes": {