UNPKG

@swrpg-online/react-dice

Version:

React components for displaying Star Wars RPG narrative and numeric dice assets

303 lines (290 loc) 13.5 kB
'use strict'; var React = require('react'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * @fileoverview A React component for rendering various types of dice (numeric and narrative) * with support for different themes and formats. */ /** Default theme for dice rendering */ var DEFAULT_THEME = 'white-arabic'; /** Default format for dice assets */ var DEFAULT_FORMAT = 'svg'; /** Default variant for D4 dice */ var DEFAULT_D4_VARIANT = 'standard'; /** Array of valid numeric die types */ var VALID_NUMERIC_DICE = ['d4', 'd6', 'd8', 'd12', 'd20', 'd100']; /** * Gets the maximum face value for a given die type */ var getMaxFace = function (type) { switch (type) { case 'd4': return 4; case 'd6': return 6; case 'd8': return 8; case 'd12': return 12; case 'd20': return 20; case 'd100': return 90; // d100 uses 0-90 in steps of 10 default: return 1; } }; /** * Formats the face number as a two-digit string */ var formatFaceNumber = function (face) { return face.toString().padStart(2, '0'); }; /** * Determines the configuration for D4 dice based on the variant. * * @param variant - The D4 variant type ('apex', 'base', or 'standard') * @returns The configuration string for the D4 variant */ var getD4Config = function (variant) { switch (variant) { case 'apex': return 'D4Apex'; case 'base': return 'D4Base'; default: return 'D4'; } }; /** * Extracts theme components from the theme string. * For example, 'white-arabic' becomes { style: 'White', script: 'Arabic' } */ var parseTheme = function (theme) { var _a = theme.split('-'), style = _a[0], script = _a[1]; return { style: style.charAt(0).toUpperCase() + style.slice(1), script: script.charAt(0).toUpperCase() + script.slice(1) }; }; /** * Gets the proper die type name for the asset path */ var getDieTypeName = function (type) { switch (type) { case 'boost': return 'Boost'; case 'proficiency': return 'Proficiency'; case 'ability': return 'Ability'; case 'setback': return 'Setback'; case 'challenge': return 'Challenge'; case 'difficulty': return 'Difficulty'; default: return type.replace('d', 'D'); } }; /** * A React component that renders dice for tabletop gaming applications. * Supports both numeric (d4, d6, etc.) and narrative dice types with various themes and formats. * * Features: * - Supports SVG and image formats * - Handles multiple D4 variants * - Provides loading and error states * - Prevents race conditions in async loading * - Supports custom styling and theming * - Implements proper cleanup on unmount * * @component * @example * // Render a standard d20 showing face 20 * <Die type="d20" face={20} /> * * @example * // Render a boost die showing two advantages * <Die * type="boost" * face="Advantage-Advantage" * className="large-die" * style={{ width: '100px' }} * /> */ var Die = function (_a) { var type = _a.type, _b = _a.face, face = _b === void 0 ? 1 : _b, _c = _a.format, format = _c === void 0 ? DEFAULT_FORMAT : _c, _d = _a.theme, theme = _d === void 0 ? DEFAULT_THEME : _d, _e = _a.variant, variant = _e === void 0 ? DEFAULT_D4_VARIANT : _e, className = _a.className, style = _a.style; /** Stores the loaded SVG component */ var _f = React__namespace.useState(null), DiceComponent = _f[0], setDiceComponent = _f[1]; /** Tracks the current loading state of the die asset */ var _g = React__namespace.useState('idle'), loadingState = _g[0], setLoadingState = _g[1]; /** Stores any error message during loading */ var _h = React__namespace.useState(null), error = _h[0], setError = _h[1]; React__namespace.useEffect(function () { var isMounted = true; var loadAsset = function () { return __awaiter(void 0, void 0, void 0, function () { var isNumericDie, assetPath, maxFace, _a, themeStyle, themeScript, d4Config, baseName, dieTypeName, module_1, error_1, errorMessage; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 2, , 3]); setLoadingState('loading'); setError(null); isNumericDie = VALID_NUMERIC_DICE.includes(type); // Validate die type and face value if (!isNumericDie && !type.startsWith('narrative-')) { throw new Error("Invalid die type: ".concat(type, ". Must be one of ").concat(VALID_NUMERIC_DICE.join(', '), " or start with 'narrative-'")); } assetPath = void 0; if (isNumericDie) { // Validate numeric face value if (typeof face !== 'number') { throw new Error("Numeric dice require a number for the face value, got: ".concat(face)); } maxFace = getMaxFace(type); if (type === 'd100') { if (face < 0 || face > maxFace || face % 10 !== 0) { throw new Error("Invalid face for d100: ".concat(face, ". Must be between 0 and 90 in steps of 10.")); } } else if (face < 1 || face > maxFace) { throw new Error("Invalid face for ".concat(type, ": ").concat(face, ". Must be between 1 and ").concat(maxFace, ".")); } _a = parseTheme(theme), themeStyle = _a.style, themeScript = _a.script; if (type === 'd4') { d4Config = getD4Config(variant); assetPath = "@swrpg-online/art/dice/numeric/".concat(theme, "/").concat(d4Config, "-").concat(formatFaceNumber(face), "-").concat(themeStyle, "-").concat(themeScript, ".").concat(format); } else { baseName = type.replace('d', 'D'); assetPath = "@swrpg-online/art/dice/numeric/".concat(theme, "/").concat(baseName, "-").concat(formatFaceNumber(face), "-").concat(themeStyle, "-").concat(themeScript, ".").concat(format); } } else { // Validate narrative face value if (typeof face !== 'string') { throw new Error("Narrative dice require a string for the face value, got: ".concat(face)); } dieTypeName = getDieTypeName(type); assetPath = "@swrpg-online/art/dice/narrative/".concat(dieTypeName, "-").concat(face, ".").concat(format); } return [4 /*yield*/, import(/* @vite-ignore */ assetPath)]; case 1: module_1 = _b.sent(); if (isMounted) { setDiceComponent(function () { return module_1.default; }); setLoadingState('success'); } return [3 /*break*/, 3]; case 2: error_1 = _b.sent(); if (isMounted) { errorMessage = error_1 instanceof Error ? error_1.message : 'Unknown error occurred'; console.error("Failed to load die ".concat(format, ":"), errorMessage); setError(errorMessage); setLoadingState('error'); setDiceComponent(null); } return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }); }; loadAsset(); return function () { isMounted = false; }; }, [type, face, format, theme, variant]); // Render error state if (loadingState === 'error') { return (React__namespace.createElement("div", { className: className, style: style, role: "alert" }, "Error loading die: ", error)); } // Render loading state if (loadingState === 'loading' || loadingState === 'idle') { return (React__namespace.createElement("div", { className: className, style: style, role: "status" }, React__namespace.createElement("span", null, "Loading ", type, " die..."))); } // Render the SVG component if (DiceComponent) { return React__namespace.createElement(DiceComponent, { className: className, style: style }); } return null; }; exports.Die = Die; //# sourceMappingURL=index.js.map