├── src ├── fonts │ └── .gitkeep ├── assets │ ├── .gitkeep │ ├── initiative.svg │ ├── inspiration.svg │ ├── armor-class.hbs │ ├── armor-class.svg │ ├── proficiency-bonus.svg │ ├── strength.svg │ └── strength.hbs ├── styles │ ├── .gitkeep │ ├── _variables.scss │ ├── _mixins.scss │ ├── spellbook.scss │ ├── actions.scss │ ├── attributes.scss │ ├── tab-area.scss │ ├── personality.scss │ ├── ability-scores.scss │ ├── biography.scss │ ├── header.scss │ ├── skills.scss │ ├── health-armor.scss │ └── inventory.scss ├── templates │ ├── .gitkeep │ ├── character-sheet-ltd.hbs │ ├── parts │ │ ├── sheet-header.hbs │ │ ├── actor-skills.hbs │ │ ├── actor-traits.hbs │ │ ├── actor-health-armor.hbs │ │ ├── actor-features.hbs │ │ ├── actor-inventory.hbs │ │ └── actor-spellbook.hbs │ └── character-sheet.hbs ├── constants.ts ├── helpers.ts ├── lang │ └── en.json ├── module │ ├── preloadTemplates.ts │ └── settings.ts ├── global.d.ts ├── module.json ├── foundryvtt-5eOGLCharacterSheet.scss └── foundryvtt-5eOGLCharacterSheet.ts ├── .eslintignore ├── .prettierignore ├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ ├── get-version.js │ └── main.yml ├── readme-img ├── biography.png ├── main-top.png ├── spellbook.png ├── cover-image.png └── main-bottom.png ├── .vscode ├── extensions.json └── settings.json ├── .prettierrc ├── tsconfig.json ├── CONTRIBUTING.md ├── .eslintrc ├── package.json ├── LICENSE ├── README.md └── gulpfile.js /src/fonts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | gulpfile.js -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore all HBS files 2 | *.hbs -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | foundryconfig.json 3 | dist -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: ElfFriend_DnD 4 | ko_fi: elffriend 5 | -------------------------------------------------------------------------------- /readme-img/biography.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/HEAD/readme-img/biography.png -------------------------------------------------------------------------------- /readme-img/main-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/HEAD/readme-img/main-top.png -------------------------------------------------------------------------------- /readme-img/spellbook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/HEAD/readme-img/spellbook.png -------------------------------------------------------------------------------- /readme-img/cover-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/HEAD/readme-img/cover-image.png -------------------------------------------------------------------------------- /readme-img/main-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/HEAD/readme-img/main-bottom.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "andrejunges.handlebars"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/get-version.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | console.log(JSON.parse(fs.readFileSync('./dist/module.json', 'utf8')).version); 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2, 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["DOM", "ES6", "ES2017"], 5 | "types": ["foundry-pc-types"] 6 | }, 7 | "include": ["global.d.ts", "./src"] 8 | } 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## VS Code 2 | Recommended extensions and workspace settings should force format on save. 3 | 4 | ## Lint before you push 5 | 6 | 1. `npm install` 7 | 2. `npm run lint` 8 | 3. ??? 9 | 4. Profit 10 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const MODULE_ID = '5e-ogl-character-sheet'; 2 | 3 | export enum MySettings { 4 | showIconsOnInventoryList = 'show-icons-on-inventory-list', 5 | showEquipOnInventoryList = 'show-equip-on-inventory-list', 6 | expandedLimited = 'expanded-limited', 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": true 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.detectIndentation": false, 7 | "editor.formatOnSave": true, 8 | "editor.insertSpaces": true, 9 | "editor.tabSize": 2 10 | } 11 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { MODULE_ID } from './constants'; 2 | 3 | export function log(force: boolean, ...args) { 4 | //@ts-ignore 5 | const shouldLog = force || game.modules.get('_dev-mode')?.api?.getPackageDebugValue(MODULE_ID); 6 | 7 | if (shouldLog) { 8 | console.log(MODULE_ID, '|', ...args); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $default-highlight: #c53131; 2 | 3 | $default-light-gray: #b5b3a4; 4 | $default-groove-gray: #eeede0; 5 | $default-accent-text: #4b4a44; 6 | $default-color: #191813; 7 | 8 | $groove-border: 2px groove var(--groove-gray); 9 | $border-radius: 6px; 10 | 11 | $space: 10px; 12 | $scrollbar-width: 6px; 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jquery": true, 4 | "browser": true, 5 | "es2020": true 6 | }, 7 | "root": true, 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": 2020, 11 | "sourceType": "module" 12 | }, 13 | "extends": ["plugin:@typescript-eslint/recommended"], 14 | "rules": { 15 | "no-underscore-dangle": "off", 16 | "import/extensions": "off", 17 | "class-methods-use-this": [ 18 | "error", 19 | { 20 | "exceptMethods": ["getData", "_updateObject"] 21 | } 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | @mixin spaced($spacing: $space) { 4 | > :not(:last-child) { 5 | margin-right: $spacing; 6 | } 7 | } 8 | 9 | @mixin spaced-vertical($spacing: $space) { 10 | > :not(:last-child) { 11 | margin-bottom: $spacing; 12 | } 13 | } 14 | 15 | @function asset($file-name) { 16 | @return url('assets/#{$file-name}'); 17 | } 18 | 19 | @mixin background-section { 20 | border-radius: $border-radius; 21 | background: var(--groove-gray); 22 | padding: $space / 2; 23 | } 24 | 25 | @mixin truncate { 26 | overflow: hidden; 27 | text-overflow: ellipsis; 28 | white-space: nowrap; 29 | } 30 | -------------------------------------------------------------------------------- /src/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "OGL5E.showIconsOnInventoryList": "Add Icons to Inventory Rows", 3 | "OGL5E.showIconsOnInventoryListHint": "Add the icons to the items on the inventory list rows.", 4 | "OGL5E.showEquipOnInventoryList": "Add Equip Check to Inventory Rows", 5 | "OGL5E.showEquipOnInventoryListHint": "Add a checkbox to equip/unequip items directly on the inventory list. By default these are in the expanded item details.", 6 | "OGL5E.expandedLimited": "Use Expanded Sheet as Limited", 7 | "OGL5E.expandedLimitedHint": "Show the full character sheet for users who have the Limited permission for a given Actor.", 8 | "OGL5E.experience": "Experience" 9 | } -------------------------------------------------------------------------------- /src/styles/spellbook.scss: -------------------------------------------------------------------------------- 1 | @import './mixins'; 2 | @import './variables'; 3 | 4 | .spellbook { 5 | .spellbook-header { 6 | h4 { 7 | flex: 0 120px; 8 | } 9 | } 10 | 11 | .item-controls { 12 | flex: 0 0 70px; 13 | } 14 | } 15 | 16 | .spellcasting-ability { 17 | flex: 0 auto; 18 | display: flex; 19 | align-items: center; 20 | font-size: 12px; 21 | 22 | > div { 23 | flex: 1; 24 | white-space: nowrap; 25 | } 26 | 27 | label { 28 | font-weight: 700; 29 | color: var(--accent-text); 30 | } 31 | } 32 | 33 | .spell-comps { 34 | @include spaced(0.2em); 35 | } 36 | 37 | .spell-target { 38 | flex: 0 0 140px; 39 | display: flex; 40 | flex-direction: column; 41 | line-height: normal; 42 | place-content: center; 43 | } 44 | 45 | .spell-comps { 46 | flex: 0 0 120px; 47 | } 48 | 49 | .spell-slots { 50 | border-right: 0; 51 | } 52 | -------------------------------------------------------------------------------- /src/styles/actions.scss: -------------------------------------------------------------------------------- 1 | @import './mixins'; 2 | @import './variables'; 3 | 4 | .actions { 5 | .item-name { 6 | flex: 3; 7 | } 8 | 9 | .item-uses { 10 | flex: 0 auto; 11 | padding-right: 0.2em; 12 | margin-left: 0.2em; 13 | } 14 | 15 | .damage-cell { 16 | overflow: hidden; 17 | white-space: nowrap; 18 | text-overflow: ellipsis; 19 | margin-left: 0.2em; 20 | flex: 2; 21 | } 22 | 23 | .action-range { 24 | display: none !important; 25 | } 26 | 27 | .to-hit-cell { 28 | text-overflow: initial; 29 | flex-basis: 70px; 30 | white-space: unset; 31 | word-wrap: unset; 32 | } 33 | 34 | .inventory-list { 35 | padding: 0; 36 | } 37 | 38 | .item-controls { 39 | width: 100%; 40 | flex: 1 0 100%; 41 | order: 16; 42 | place-content: flex-end; 43 | 44 | &:last-child { 45 | display: none; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/module/preloadTemplates.ts: -------------------------------------------------------------------------------- 1 | import { MODULE_ID } from '../constants.js'; 2 | 3 | export const preloadTemplates = async function () { 4 | const templatePaths = [ 5 | `modules/${MODULE_ID}/assets/armor-class.hbs`, 6 | `modules/${MODULE_ID}/assets/strength.hbs`, 7 | `modules/${MODULE_ID}/templates/character-sheet-ltd.hbs`, 8 | `modules/${MODULE_ID}/templates/character-sheet.hbs`, 9 | `modules/${MODULE_ID}/templates/parts/actor-health-armor.hbs`, 10 | `modules/${MODULE_ID}/templates/parts/actor-skills.hbs`, 11 | `modules/${MODULE_ID}/templates/parts/actor-features.hbs`, 12 | `modules/${MODULE_ID}/templates/parts/actor-inventory.hbs`, 13 | `modules/${MODULE_ID}/templates/parts/actor-spellbook.hbs`, 14 | `modules/${MODULE_ID}/templates/parts/actor-traits.hbs`, 15 | `modules/${MODULE_ID}/templates/parts/sheet-header.hbs`, 16 | ]; 17 | 18 | return loadTemplates(templatePaths); 19 | }; 20 | -------------------------------------------------------------------------------- /src/styles/attributes.scss: -------------------------------------------------------------------------------- 1 | @import './mixins'; 2 | @import './variables'; 3 | 4 | .attributes { 5 | flex: 0 0 auto; 6 | height: auto; 7 | 8 | display: grid; 9 | max-height: 30%; 10 | overflow: overlay; 11 | grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); 12 | grid-gap: $space / 2; 13 | 14 | .attribute { 15 | margin: 0; 16 | width: 100%; 17 | } 18 | } 19 | 20 | .attribute { 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: space-between; 24 | height: auto; 25 | 26 | .attribute-value { 27 | font-size: 2em; 28 | margin-top: auto; 29 | margin-bottom: auto; 30 | } 31 | } 32 | 33 | .attribute-footer { 34 | display: flex; 35 | align-items: center; 36 | min-height: 1.5em; // arbitrary 37 | justify-content: space-around; 38 | 39 | input { 40 | flex: 1; 41 | } 42 | } 43 | 44 | .resource { 45 | .attribute-value { 46 | input { 47 | flex: 0 auto; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/assets/initiative.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "foundryvtt-5eOGLCharacterSheet", 4 | "version": "0.14.1", 5 | "description": "A Character Sheet for D&D 5e that resembles the OGL 5e Sheet", 6 | "scripts": { 7 | "publish": "gulp publish", 8 | "lint": "prettier --write .", 9 | "build": "gulp build", 10 | "build:watch": "gulp watch", 11 | "clean": "gulp clean && gulp link --clean", 12 | "update": "npm install --save-dev gitlab:foundry-projects/foundry-pc/foundry-pc-types" 13 | }, 14 | "author": "", 15 | "license": "", 16 | "devDependencies": { 17 | "archiver": "^5.0.2", 18 | "chalk": "^4.1.0", 19 | "foundry-pc-types": "gitlab:foundry-projects/foundry-pc/foundry-pc-types", 20 | "fs-extra": "^9.0.1", 21 | "gulp": "^4.0.2", 22 | "gulp-git": "^2.10.1", 23 | "gulp-less": "^4.0.1", 24 | "gulp-sass": "^4.1.0", 25 | "gulp-typescript": "^6.0.0-alpha.1", 26 | "json-stringify-pretty-compact": "^2.0.0", 27 | "sass": "^1.26.11", 28 | "typescript": "^4.0.3", 29 | "yargs": "^16.0.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/module/settings.ts: -------------------------------------------------------------------------------- 1 | import { MySettings, MODULE_ID } from '../constants'; 2 | 3 | export const registerSettings = function () { 4 | // Register any custom module settings here 5 | game.settings.register(MODULE_ID, MySettings.showIconsOnInventoryList, { 6 | name: 'OGL5E.showIconsOnInventoryList', 7 | default: false, 8 | type: Boolean, 9 | scope: 'client', 10 | config: true, 11 | hint: 'OGL5E.showIconsOnInventoryListHint', 12 | }); 13 | 14 | game.settings.register(MODULE_ID, MySettings.showEquipOnInventoryList, { 15 | name: 'OGL5E.showEquipOnInventoryList', 16 | default: false, 17 | type: Boolean, 18 | scope: 'client', 19 | config: true, 20 | hint: 'OGL5E.showEquipOnInventoryListHint', 21 | }); 22 | 23 | game.settings.register(MODULE_ID, MySettings.expandedLimited, { 24 | name: 'OGL5E.expandedLimited', 25 | default: false, 26 | type: Boolean, 27 | scope: 'world', 28 | config: true, 29 | hint: 'OGL5E.expandedLimitedHint', 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Elf Friend D&D 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/styles/tab-area.scss: -------------------------------------------------------------------------------- 1 | @import './mixins'; 2 | @import './variables'; 3 | 4 | .sheet-navigation { 5 | @include spaced($space); 6 | 7 | flex: 0 auto; 8 | flex-wrap: nowrap; 9 | font-family: inherit; 10 | font-size: 14px; 11 | overflow: overlay; 12 | text-transform: uppercase; 13 | 14 | .item { 15 | color: var(--accent-text); 16 | flex: 0 auto; 17 | height: auto; 18 | line-height: normal; 19 | margin: 0; 20 | padding: 0 0.4em; 21 | width: max-content; 22 | 23 | &:not(.active) { 24 | border-bottom-color: transparent; 25 | } 26 | 27 | &.active, 28 | &:hover { 29 | text-shadow: none; 30 | } 31 | 32 | &.active { 33 | border-bottom-width: 2px; 34 | color: inherit; 35 | border-bottom-color: var(--highlight); 36 | } 37 | } 38 | } 39 | 40 | .filter-list { 41 | @include spaced($space / 2); 42 | 43 | line-height: normal; 44 | } 45 | 46 | .filter-item { 47 | border-bottom: 1px groove var(--groove-gray); 48 | display: flex; 49 | align-content: center; 50 | } 51 | 52 | .tab { 53 | @include spaced-vertical($space / 2); 54 | } 55 | 56 | .inventory-filters { 57 | justify-content: start; 58 | flex: 0 auto; 59 | margin: 0; 60 | } 61 | -------------------------------------------------------------------------------- /src/templates/character-sheet-ltd.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

5 | 6 |

7 |
8 | 9 |
10 | 11 | {{!-- Biography Tab --}} 12 |
13 |
14 |
15 |

{{ localize "DND5E.Appearance" }}

16 | {{editor appearance target="system.details.appearance" button=true editable=editable engine="prosemirror" collaborate=false}} 17 |
18 |
19 | 20 |
21 |
22 |

{{ localize "DND5E.Background" }}/{{ localize "DND5E.Biography" }}

23 | {{editor biographyHTML target="system.details.biography.value" button=true editable=editable engine="prosemirror" collaborate=false}} 24 |
25 |
26 | 27 |
28 |
29 |
-------------------------------------------------------------------------------- /src/assets/inspiration.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /src/styles/personality.scss: -------------------------------------------------------------------------------- 1 | @import './mixins'; 2 | @import './variables'; 3 | 4 | .personality { 5 | @include spaced-vertical($space / 4); 6 | 7 | h4 { 8 | font-size: 1em; 9 | font-weight: 700; 10 | } 11 | 12 | section { 13 | border-radius: 0; 14 | display: flex; 15 | flex-direction: column; 16 | flex: 0 1 auto; 17 | padding: $space / 2; 18 | 19 | h4 { 20 | flex: 0 auto; 21 | } 22 | 23 | .editor { 24 | flex: 1; 25 | padding: 0; 26 | min-height: 50px; 27 | } 28 | 29 | &:first-child { 30 | border-top-left-radius: $border-radius; 31 | border-top-right-radius: $border-radius; 32 | } 33 | 34 | &:last-child { 35 | border-bottom-left-radius: $border-radius; 36 | border-bottom-right-radius: $border-radius; 37 | } 38 | } 39 | 40 | .big-bio { 41 | flex: 1; 42 | } 43 | 44 | .editor { 45 | h1, 46 | h2, 47 | h3 { 48 | font-weight: 700; 49 | font-family: 'Modesto Condensed', 'Palatino Linotype', serif; 50 | } 51 | 52 | p { 53 | margin: 0; 54 | &:not(:last-child) { 55 | margin-bottom: 0.5em; 56 | } 57 | } 58 | 59 | h1 { 60 | font-size: 2em; 61 | } 62 | 63 | // editor overrides, RISKY 64 | .tox { 65 | height: 100% !important; 66 | 67 | .tox-toolbar__group { 68 | padding: 0; 69 | } 70 | 71 | .tox-tbtn { 72 | height: 2em; 73 | 74 | &:not(.tox-tbtn--select) { 75 | width: 2em; 76 | } 77 | } 78 | 79 | .tox-sidebar-wrap { 80 | min-height: 5em; 81 | flex: 1 1 5em; 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/styles/ability-scores.scss: -------------------------------------------------------------------------------- 1 | @import './mixins'; 2 | @import './variables'; 3 | 4 | .ability-scores { 5 | height: auto; 6 | font-size: inherit; 7 | 8 | .ability { 9 | display: flex; 10 | flex-direction: column; 11 | height: auto; 12 | min-width: 70px; // arbitrary 13 | height: 69px; // lines things up with fancy outline 14 | position: relative; 15 | border: 0; 16 | 17 | &:last-child { 18 | margin-bottom: 0; 19 | } 20 | 21 | .ability-name { 22 | position: relative; 23 | border-bottom: 0; 24 | } 25 | 26 | .ability-score { 27 | position: relative; 28 | } 29 | 30 | .ability-modifiers { 31 | height: auto; 32 | justify-content: center; 33 | margin: 0; 34 | position: absolute; 35 | top: calc(100% - 0.8em); 36 | width: 100%; 37 | 38 | span.ability-mod { 39 | border: 0; 40 | line-height: normal; 41 | border-radius: 50%; 42 | } 43 | } 44 | } 45 | } 46 | 47 | .ability-score-column { 48 | @include background-section; 49 | @include spaced-vertical($space * 2); 50 | 51 | border: 0; 52 | display: flex; 53 | flex-direction: column; 54 | flex: 0 0 auto; 55 | height: auto; 56 | padding-top: $space; 57 | padding-bottom: $space * 2; 58 | 59 | > li { 60 | flex: 1 0 auto; 61 | padding-bottom: 0.8em; 62 | } 63 | } 64 | 65 | .ability-outline { 66 | bottom: 0; 67 | left: 0; 68 | pointer-events: none; 69 | position: absolute; 70 | right: 0; 71 | top: 0; 72 | 73 | > svg { 74 | max-width: 100%; 75 | 76 | .cls-1, 77 | .cls-3 { 78 | fill: var(--groove-gray); 79 | } 80 | 81 | .cls-2, 82 | .cls-3 { 83 | stroke: currentColor; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/styles/biography.scss: -------------------------------------------------------------------------------- 1 | @import './mixins'; 2 | @import './variables'; 3 | 4 | .biography { 5 | @include spaced($space); 6 | 7 | max-width: unset; 8 | 9 | h4 { 10 | font-size: 1em; 11 | font-weight: 700; 12 | } 13 | 14 | .note-entries { 15 | @include spaced-vertical($space); 16 | } 17 | 18 | .left-notes { 19 | flex: 2; 20 | } 21 | 22 | .profile { 23 | width: 50%; 24 | height: auto; 25 | border: 0; 26 | margin-left: auto; 27 | margin-right: auto; 28 | } 29 | 30 | .right-notes { 31 | flex: 3; 32 | } 33 | 34 | article { 35 | border-radius: $border-radius; 36 | display: flex; 37 | flex-direction: column; 38 | flex: 1 auto; 39 | padding: $space / 2; 40 | 41 | h4 { 42 | flex: 0 auto; 43 | border-bottom: $groove-border; 44 | } 45 | 46 | .editor { 47 | flex: 1 50px; 48 | padding: 0; 49 | } 50 | } 51 | 52 | .big-bio { 53 | flex: 1; 54 | } 55 | 56 | .editor { 57 | h1, 58 | h2, 59 | h3 { 60 | font-weight: 700; 61 | font-family: 'Modesto Condensed', 'Palatino Linotype', serif; 62 | } 63 | 64 | p { 65 | margin: 0; 66 | &:not(:last-child) { 67 | margin-bottom: 0.5em; 68 | } 69 | } 70 | 71 | h1 { 72 | font-size: 2em; 73 | } 74 | 75 | // editor overrides, RISKY 76 | .tox { 77 | height: 100% !important; 78 | 79 | .tox-toolbar__group { 80 | padding: 0; 81 | } 82 | 83 | .tox-tbtn { 84 | height: 2em; 85 | 86 | &:not(.tox-tbtn--select) { 87 | width: 2em; 88 | } 89 | } 90 | 91 | .tox-sidebar-wrap { 92 | min-height: 5em; 93 | flex: 1 1 5em; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/assets/armor-class.hbs: -------------------------------------------------------------------------------- 1 | 2 | Artboard 1 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/armor-class.svg: -------------------------------------------------------------------------------- 1 | 2 | Artboard 1 3 | 4 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /* This is by no means an accurate type for the 5e system, but it is enough of a start for ts to not complain */ 2 | 3 | interface ItemData5e extends ItemData { 4 | equipped?: boolean; 5 | activation?: { 6 | type?: 'action' | 'bonus' | 'reaction'; 7 | }; 8 | preparation?: { 9 | mode: 'always' | 'prepared'; 10 | prepared: boolean; 11 | }; 12 | damage?: { 13 | parts: string[][]; 14 | }; 15 | ability?: 'str' | 'dex' | 'con' | 'int' | 'wis' | 'cha'; 16 | actionType?: 'mwak' | 'rwak' | 'rsak' | 'msak' | 'save'; 17 | properties?: { 18 | fin?: boolean; 19 | }; 20 | weaponType?: 'simpleM' | 'martialM' | 'simpleR' | 'martialR' | 'natural' | 'improv' | 'siege'; 21 | } 22 | 23 | interface Item5e extends Item { 24 | data: ItemData5e; 25 | labels: Record; 26 | } 27 | 28 | interface ActorSheet5eCharacterSheetDataType extends ActorData { 29 | abilities: Record< 30 | 'str' | 'dex' | 'con' | 'int' | 'wis' | 'cha', 31 | { 32 | mod: number; 33 | } 34 | >; 35 | attributes: { 36 | prof?: number; 37 | spellcasting?: 'str' | 'dex' | 'con' | 'int' | 'wis' | 'cha'; 38 | }; 39 | bonuses: Record<'mwak' | 'rwak' | 'rsak' | 'msak', { attack?: string; damage?: string }> & { 40 | save: { 41 | dc: number; 42 | }; 43 | }; 44 | } 45 | 46 | interface ActorSheet5eCharacterSheetData extends ActorSheetData { 47 | data: ActorSheet5eCharacterSheetDataType; 48 | inventory: { 49 | label: string; 50 | items: Item5e[]; 51 | }[]; 52 | spellbook?: { 53 | label: string; 54 | spells: Item5e[]; 55 | }[]; 56 | features: { 57 | label: string; 58 | items: Item5e[]; 59 | }[]; 60 | } 61 | 62 | declare class ActorSheet5eCharacter extends ActorSheet { 63 | sheetData: ActorSheet5eCharacterSheetData; 64 | getData(): ActorSheet5eCharacterSheetData; 65 | } 66 | 67 | /* module specific types */ 68 | 69 | interface OGL5eCharacterSheetSheetData extends ActorSheet5eCharacterSheetData { 70 | actionsData: Record>; 71 | } 72 | 73 | declare class OGL5eCharacterSheet extends ActorSheet5eCharacter { 74 | sheetData: OGL5eCharacterSheetSheetData; 75 | getData(): OGL5eCharacterSheetSheetData; 76 | } 77 | -------------------------------------------------------------------------------- /src/styles/header.scss: -------------------------------------------------------------------------------- 1 | @import './mixins'; 2 | @import './variables'; 3 | 4 | .sheet-header { 5 | @include spaced($space); 6 | 7 | border-bottom: 0; 8 | flex: 0; 9 | display: flex; 10 | align-items: center; 11 | 12 | h1 { 13 | flex: 1; 14 | height: auto; 15 | padding: 0; 16 | font-size: 1.6em; 17 | font-weight: 700; 18 | 19 | input { 20 | font-size: inherit; 21 | height: 100%; 22 | line-height: normal; 23 | margin: 0; 24 | padding: 0; 25 | width: 100%; 26 | } 27 | } 28 | } 29 | 30 | .header-actions { 31 | @include spaced($space / 2); 32 | 33 | display: flex; 34 | justify-content: space-between; 35 | font-size: 12px; 36 | font-family: 'Signika', sans-serif; 37 | flex: 0 auto; 38 | 39 | .rest { 40 | align-items: center; 41 | border: 1px solid var(--light-gray); 42 | border-radius: $border-radius; 43 | display: inline-flex; 44 | padding: 0.1em 0.2em; 45 | 46 | &:hover, 47 | &:focus { 48 | border-color: var(--highlight); 49 | background-color: rgba(var(--highlight), 0.1); 50 | } 51 | 52 | i { 53 | margin-left: 0.2em; 54 | } 55 | } 56 | } 57 | 58 | .header-fields { 59 | align-items: center; 60 | color: var(--accent-text); 61 | display: inline-flex; 62 | flex: 2; 63 | list-style: none; 64 | margin: 0; 65 | padding: 0; 66 | flex-wrap: wrap; 67 | li { 68 | display: flex; 69 | flex-direction: column; 70 | margin-top: $space; 71 | 72 | input { 73 | color: inherit; 74 | font-weight: inherit; 75 | padding: 0; 76 | } 77 | 78 | .non-editable { 79 | display: block; 80 | } 81 | 82 | > :first-child { 83 | border-bottom: 1px solid var(--accent-text); 84 | border-left: 0; 85 | border-right: 0; 86 | border-top: 0; 87 | border-radius: 0; 88 | width: 100%; 89 | } 90 | } 91 | 92 | .alignment { 93 | flex: 1 50%; 94 | } 95 | .background { 96 | flex-basis: 25%; 97 | } 98 | .class { 99 | flex-basis: 75%; 100 | } 101 | .experience-input { 102 | flex-basis: 25%; 103 | } 104 | .race { 105 | flex-basis: 25%; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Module CI/CD 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Use Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: '12.x' 18 | 19 | - name: Install dependencies 20 | run: npm ci 21 | 22 | # run our gulp build action 23 | - name: Build dist directory 24 | run: npm run build 25 | 26 | # create a zip file with all files required by the module to add to the release 27 | - name: Zip Files 28 | working-directory: ./dist 29 | run: zip -r ./module.zip ./* 30 | 31 | # Get the version from 'module.json' 32 | - name: Get Version 33 | shell: bash 34 | id: get-version 35 | run: echo "::set-output name=version::$(node ./.github/workflows/get-version.js)" 36 | 37 | # Generate changelog for release body 38 | - name: Changelog 39 | id: Changelog 40 | uses: scottbrenner/generate-changelog-action@master 41 | env: 42 | REPO: ${{ github.repository }} 43 | 44 | # Create a release for this specific version 45 | - name: Create Release 46 | id: create_version_release 47 | uses: ncipollo/release-action@v1 48 | with: 49 | allowUpdates: true # set this to false if you want to prevent updating existing releases 50 | name: ${{ steps.get-version.outputs.version }} 51 | body: | 52 | ${{ steps.Changelog.outputs.changelog }} 53 | draft: false 54 | prerelease: false 55 | token: ${{ secrets.GITHUB_TOKEN }} 56 | artifacts: './dist/module.json,./dist/module.zip' 57 | tag_name: ${{ steps.get-version.outputs.version }} 58 | 59 | # Update the 'latest' release 60 | - name: Create Release 61 | id: create_latest_release 62 | uses: ncipollo/release-action@v1 63 | if: endsWith(github.ref, 'master') 64 | with: 65 | allowUpdates: true 66 | name: Latest 67 | draft: false 68 | prerelease: false 69 | token: ${{ secrets.GITHUB_TOKEN }} 70 | artifacts: './dist/module.json, ./dist/module.zip' 71 | tag: latest 72 | -------------------------------------------------------------------------------- /src/templates/parts/sheet-header.hbs: -------------------------------------------------------------------------------- 1 | {{!-- Sheet Header --}} 2 |
3 | {{> "systems/dnd5e/templates/actors/parts/actor-warnings.html"}} 4 | 5 |

6 | 7 |

8 | 9 | {{!-- Character Summary --}} 10 |
    11 |
  • 12 | {{classLabels}} 13 | {{ localize "DND5E.ItemTypeClass" }} & {{ localize "DND5E.Level" }} 14 |
  • 15 |
  • 16 | 18 | 19 |
  • 20 |
  • 21 | 23 | 24 |
  • 25 |
  • 26 | 28 | 29 |
  • 30 | {{#unless disableExperience}} 31 |
  • 32 | 34 | 35 |
  • 36 | {{/unless}} 37 |
38 |
39 | 40 | -------------------------------------------------------------------------------- /src/styles/skills.scss: -------------------------------------------------------------------------------- 1 | @import './mixins'; 2 | @import './variables'; 3 | 4 | .skills-column { 5 | @include spaced-vertical; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | ul.skills-list { 12 | border: 0; 13 | flex: 0; 14 | height: auto; 15 | margin: 0; 16 | padding: 0; 17 | 18 | li.skill { 19 | align-items: center; 20 | height: auto; 21 | line-height: normal; 22 | padding: 0; 23 | 24 | &:nth-child(even) { 25 | background-color: var(--darker-background); 26 | border-radius: $border-radius / 2; 27 | } 28 | 29 | .skill-proficiency, 30 | .ability-proficiency { 31 | line-height: normal; 32 | flex: 0 0 16px; 33 | } 34 | 35 | .skill-ability { 36 | font-size: 0.8em; 37 | letter-spacing: 0.01em; 38 | opacity: 0.6; 39 | text-transform: uppercase; 40 | 41 | &::before { 42 | content: '('; 43 | } 44 | 45 | &::after { 46 | content: ')'; 47 | } 48 | } 49 | } 50 | } 51 | 52 | .traits { 53 | label { 54 | flex: 1; 55 | font-size: 12px; 56 | margin-right: auto; 57 | } 58 | 59 | em { 60 | flex: 1 100%; 61 | } 62 | } 63 | 64 | .fancy-value { 65 | align-items: center; 66 | color: var(--accent-text); 67 | display: flex; 68 | font-family: 'Modesto Condensed', 'Palatino Linotype', serif; 69 | font-size: 16px; 70 | font-weight: 700; 71 | 72 | .fas { 73 | margin: 0; 74 | } 75 | 76 | h4 { 77 | border: 1px solid var(--light-gray); 78 | border-radius: 4px 0 0 4px; 79 | border-right: 0; 80 | flex: 1; 81 | font-weight: inherit; 82 | margin: 0; 83 | padding: 0.1em 0.2em; 84 | } 85 | 86 | > div { 87 | align-self: stretch; 88 | border: 1px solid var(--light-gray); 89 | border-radius: 50%; 90 | display: flex; 91 | flex: 0 0 1.8em; 92 | height: 1.6em; 93 | margin-left: calc(-1 * (0.4em + 1px)); // :rip: puppies 94 | padding: 0.2em 0.4em; 95 | place-content: center; 96 | place-items: center; 97 | position: relative; 98 | text-align: center; 99 | 100 | // hidden inspiration input 101 | > input { 102 | position: absolute; 103 | top: 0; 104 | left: 0; 105 | right: 0; 106 | bottom: 0; 107 | width: 100%; 108 | height: 100%; 109 | opacity: 0; 110 | 111 | &:hover + i { 112 | text-shadow: 0 0 8px var(--highlight); 113 | color: var(--color); 114 | } 115 | 116 | &:checked + i { 117 | color: var(--highlight); 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/styles/health-armor.scss: -------------------------------------------------------------------------------- 1 | @import './mixins'; 2 | @import './variables'; 3 | 4 | .health-armor { 5 | @include background-section; 6 | @include spaced-vertical($space / 2); 7 | 8 | border: 0; 9 | } 10 | 11 | .ac-initiative-speed { 12 | @include spaced($space / 2); 13 | 14 | list-style: none; 15 | padding: 0; 16 | margin: 0; 17 | } 18 | 19 | .ac { 20 | flex: 0 0 70px; 21 | height: 80px; // arbitrary 22 | min-width: 70px; // arbitrary based on the shield shape 23 | padding: 12px; // arbitrary to fit within shield shape 24 | position: relative; 25 | text-align: center; 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | 30 | h4 { 31 | color: var(--accent-text); 32 | font-size: 10px; 33 | font-weight: 700; 34 | margin: 0; 35 | text-transform: uppercase; 36 | } 37 | 38 | .attribute-value { 39 | line-height: 1.3; 40 | } 41 | 42 | .config-button { 43 | position: absolute; 44 | top: 0; 45 | right: 0; 46 | display: none; 47 | } 48 | 49 | &:hover { 50 | .config-button { 51 | display: block; 52 | } 53 | } 54 | 55 | .property-attribution { 56 | font-size: 14px; 57 | font-weight: normal; 58 | line-height: 1rem; 59 | min-width: 150px; 60 | top: calc(100% - 1em); 61 | } 62 | } 63 | 64 | .hd-ds { 65 | @include spaced($space / 2); 66 | } 67 | 68 | .hd { 69 | position: relative; 70 | 71 | .non-editable { 72 | flex: 0 auto; 73 | width: 100%; 74 | } 75 | 76 | &:hover { 77 | .config-button { 78 | visibility: visible; 79 | } 80 | } 81 | 82 | .config-button { 83 | visibility: hidden; 84 | position: absolute; 85 | top: $space / 2; 86 | right: 2px; 87 | font-size: 12px; // arbitrary 88 | } 89 | } 90 | 91 | .ac-symbol { 92 | bottom: 0; 93 | left: 0; 94 | pointer-events: none; 95 | position: absolute; 96 | right: 0; 97 | top: 0; 98 | 99 | > svg { 100 | max-width: 100%; 101 | max-height: 100%; 102 | 103 | path { 104 | fill: var(--light-gray); 105 | } 106 | } 107 | } 108 | 109 | .movement { 110 | position: relative; 111 | overflow: hidden; 112 | 113 | &:hover { 114 | .config-button { 115 | visibility: visible; 116 | } 117 | } 118 | 119 | .config-button { 120 | visibility: hidden; 121 | position: absolute; 122 | top: 3px; 123 | right: 2px; 124 | font-size: 10px; // arbitrary 125 | } 126 | 127 | .movement-primary { 128 | @include truncate; 129 | } 130 | 131 | .movement-special { 132 | @include truncate; 133 | } 134 | } 135 | 136 | .warnings { 137 | position: absolute; 138 | bottom: 100%; 139 | left: 0; 140 | width: 100%; 141 | } 142 | -------------------------------------------------------------------------------- /src/templates/parts/actor-skills.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "5e-ogl-character-sheet", 3 | "name": "5e-ogl-character-sheet", 4 | "title": "D&D 5e OGL Character Sheet", 5 | "description": "A character sheet resembling a classic Pencil and Paper or Roll20 sheet.", 6 | "system": ["dnd5e"], 7 | "version": "0.14.1", 8 | "author": "Andrew Krigline (akrigline)", 9 | "authors": [ 10 | { 11 | "name": "Andrew Krigline", 12 | "discord": "Calego#0914", 13 | "url": "https://github.com/ElfFriend-DnD", 14 | "patreon": "https://www.patreon.com/ElfFriend_DnD" 15 | } 16 | ], 17 | "dependencies": [ 18 | { 19 | "name": "character-actions-list-5e", 20 | "type": "module", 21 | "versionMin": "1.0.0", 22 | "manifest": "https://github.com/ElfFriend-DnD/foundryvtt-dnd5eCharacterActions/releases/latest/download/module.json" 23 | } 24 | ], 25 | "esmodules": [ 26 | "foundryvtt-5eOGLCharacterSheet.js" 27 | ], 28 | "styles": [ 29 | "foundryvtt-5eOGLCharacterSheet.css" 30 | ], 31 | "languages": [ 32 | { 33 | "lang": "en", 34 | "name": "English", 35 | "path": "lang/en.json" 36 | } 37 | ], 38 | "media": [ 39 | { 40 | "type": "cover", 41 | "link": "https://raw.githubusercontent.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/master/readme-img/cover-image.png" 42 | }, 43 | { 44 | "type": "screenshot", 45 | "link": "https://raw.githubusercontent.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/master/readme-img/main-top.png" 46 | }, 47 | { 48 | "type": "screenshot", 49 | "link": "https://raw.githubusercontent.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/master/readme-img/main-bottom.png" 50 | }, 51 | { 52 | "type": "screenshot", 53 | "link": "https://raw.githubusercontent.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/master/readme-img/spellbook.png" 54 | }, 55 | { 56 | "type": "screenshot", 57 | "link": "https://raw.githubusercontent.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/master/readme-img/biography.png" 58 | } 59 | ], 60 | "relationships": { 61 | "systems": [ 62 | { 63 | "id": "dnd5e", 64 | "type": "system", 65 | "manifest": "https://raw.githubusercontent.com/foundryvtt/dnd5e/master/system.json", 66 | "compatibility": { 67 | "minimum": "2.0.0" 68 | } 69 | } 70 | ] 71 | }, 72 | "compatibility":{ 73 | "minimum": 10, 74 | "verified": 10, 75 | "maximum": 10 76 | }, 77 | "bugs": "https://github.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/issues", 78 | "changelog": "https://github.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/releases", 79 | "download": "https://github.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/releases/download/v0.14.1/module.zip", 80 | "manifest": "https://github.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/releases/latest/download/module.json", 81 | "readme": "https://raw.githubusercontent.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/master/README.md", 82 | "url": "https://github.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet", 83 | "flags": { 84 | "allowBugReporter": true 85 | }, 86 | "manifestPlusVersion": "1.0.0" 87 | } 88 | -------------------------------------------------------------------------------- /src/templates/parts/actor-traits.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 11 |
12 | 13 | {{#*inline 'possiblyNoneList'}} 14 | {{#if (ogl5e-sheet-isEmpty list)}} 15 | {{localize "DND5E.None"}} 16 | {{else}} 17 |
    18 | {{#each list as |v k|}} 19 |
  • {{v}}
  • 20 | {{/each}} 21 |
22 | {{/if}} 23 | {{/inline}} 24 | 25 |
26 | 27 | {{#if senses}} 28 | 29 | 30 | 31 | {{> possiblyNoneList list=senses}} 32 | {{else}} 33 | 35 | {{/if}} 36 |
37 | 38 |
39 | 40 | 41 | 42 | 43 | {{> possiblyNoneList list=system.traits.languages.selected}} 44 |
45 | 46 |
47 | 48 | 49 | 50 | 51 | {{> possiblyNoneList list=system.traits.di.selected}} 52 |
53 | 54 |
55 | 56 | 57 | 58 | 59 | {{> possiblyNoneList list=system.traits.dr.selected}} 60 |
61 | 62 |
63 | 64 | 65 | 66 | 67 | {{> possiblyNoneList list=system.traits.dv.selected}} 68 |
69 | 70 |
71 | 72 | 73 | 74 | 75 | {{> possiblyNoneList list=system.traits.ci.selected}} 76 |
77 | 78 | {{#if isCharacter}} 79 |
80 | 81 | 82 | 83 | 84 | {{> possiblyNoneList list=system.traits.weaponProf.selected}} 85 |
86 | 87 |
88 | 89 | 90 | 91 | 92 | {{> possiblyNoneList list=system.traits.armorProf.selected}} 93 |
94 | 95 |
96 | 97 | 98 | 99 | 100 | {{> possiblyNoneList list=system.traits.toolProf.selected}} 101 |
102 | {{/if}} 103 | 104 | {{#unless isVehicle}} 105 |
106 | 107 | 109 |
110 | {{/unless}} 111 |
-------------------------------------------------------------------------------- /src/templates/parts/actor-health-armor.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
    4 |
  • 5 |
    6 | {{> (ogl5e-sheet-path "assets/armor-class.hbs")}} 7 |
    8 |

    Armor

    9 |
    10 | {{system.attributes.ac.value}} 11 |
    12 |

    Class

    13 | 15 |
  • 16 | 17 |
  • 18 |

    {{ localize "DND5E.Initiative" }}

    19 |
    20 | {{numberFormat system.attributes.init.total decimals=0 sign=true}} 21 |
    22 |
    23 | {{ localize "DND5E.Modifier" }} 24 | 26 |
    27 |
  • 28 | 29 |
  • 30 |

    {{ localize "DND5E.Speed" }} ({{system.attributes.movement.units}})

    31 |
    32 | {{movement.primary}} 33 |
    34 |
    35 | {{movement.special}} 36 |
    37 | 39 |
  • 40 |
41 | 42 | 43 |
44 |

{{ localize "DND5E.HitPoints" }}

45 |
46 | 48 | / 49 | 51 |
52 |
53 | 55 | 57 |
58 |
59 | 60 |
61 |
62 |

63 | {{ localize "DND5E.HitDice" }} 64 | 66 |

67 |
68 | 70 | / 71 | {{system.details.level}} 72 |
73 |
74 | {{!-- display hit die size? features[class].system.hitdice --}} 75 |
76 |
77 | 78 |
79 |

Death Saves

80 |
81 |
82 | 84 |
85 |
86 | 88 |
89 |
90 |
91 | 92 | 93 | 94 |
95 |
96 | 97 |
98 | 99 | 100 |
101 | 102 | 104 |
105 | 106 |
-------------------------------------------------------------------------------- /src/templates/parts/actor-features.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#each sections as |section sid|}} 3 |
  1. 4 |

    {{localize section.label}}

    5 | 6 | {{#if section.hasActions}} 7 |
    {{localize "DND5E.Usage"}}
    8 | {{/if}} 9 | 10 | {{#if section.columns}} 11 | {{#each section.columns}} 12 |
    {{label}}
    13 | {{/each}} 14 | {{/if}} 15 | 16 | {{#if ../owner}} 17 |
    18 | 20 | 21 | 22 |
    23 | {{/if}} 24 |
  2. 25 | 26 |
      27 | {{#each section.items as |item iid|}} 28 |
    1. 29 |
      30 | {{#if (or section.hasActions ../../settingsShowInventoryIcons)}} 31 |
      33 | {{/if}} 34 | {{#if @root/systemFeatures.subclasses}} 35 |

      36 | {{#if (eq item.type "subclass")}}↳{{/if}} 37 | {{item.name}} 38 | {{#if item.system.isOriginalClass}} {{/if}} 40 |

      41 | {{else}} 42 |

      43 | {{item.name}} 44 |

      45 | {{/if}} 46 | 47 | {{#if (eq item.type "class")}} 48 | {{#if @root/systemFeatures.levelDropdown}} 49 | 59 | {{/if}} 60 | {{/if}} 61 | 62 | {{#if (or item.isOnCooldown item.system.recharge.value item.hasUses)}} 63 |
      64 | 65 | {{#if item.isOnCooldown}} 66 | 67 | {{item.labels.recharge}} 68 | 69 | {{else if item.system.recharge.value}} 70 | 71 | {{localize "DND5E.Charged"}} 72 | 73 | {{else if item.hasUses}} 74 | 75 | / {{item.system.uses.max}} 76 | 77 | {{localize "DND5E.Uses"}} 78 | 79 | {{/if}} 80 | 81 |
      82 | {{/if}} 83 |
      84 | 85 | {{#if section.hasActions}} 86 |
      87 | {{#if item.system.activation.type }} 88 | {{!-- Use our custom abbreviated labels --}} 89 | {{item.labels.activationAbbrev}} 90 | {{/if}} 91 |
      92 | {{/if}} 93 | 94 | {{#if section.isClass}} 95 | {{#unless @root/systemFeatures.subclasses}} 96 |
      97 | {{item.system.subclass}} 98 |
      99 | {{/unless}} 100 | {{#unless @root/systemFeatures.levelDropdown}} 101 | {{#if (eq item.type "class")}} 102 |
      103 | Level {{item.system.levels}} 104 |
      105 | {{/if}} 106 | {{/unless}} 107 | {{/if}} 108 | 109 | {{#if section.columns}} 110 | {{#each section.columns}} 111 |
      112 | {{#with (getProperty item property)}} 113 | {{#if ../editable}} 114 | 115 | {{else}} 116 | {{this}} 117 | {{/if}} 118 | {{/with}} 119 |
      120 | {{/each}} 121 | {{/if}} 122 | 123 | {{#if @root/owner}} 124 |
      125 | {{#if section.crewable}} 126 | 127 | 128 | 129 | {{/if}} 130 | 131 | 132 |
      133 | {{/if}} 134 |
    2. 135 | {{/each}} 136 |
    137 | {{/each}} 138 |
-------------------------------------------------------------------------------- /src/styles/inventory.scss: -------------------------------------------------------------------------------- 1 | @import './mixins'; 2 | @import './variables'; 3 | 4 | /* Applied to all Lists on the Sheet */ 5 | .items-list { 6 | .items-header { 7 | background: none; 8 | border: 0; 9 | border-bottom: $groove-border; 10 | } 11 | 12 | .item { 13 | border-bottom: 0; 14 | 15 | &:nth-child(even) { 16 | background-color: var(--darker-background); 17 | border-radius: $border-radius / 2; 18 | } 19 | } 20 | 21 | .item-list { 22 | &:not(:last-of-type) { 23 | margin-bottom: $space / 2; 24 | } 25 | 26 | .item-image-show { 27 | display: block; 28 | box-sizing: inherit; 29 | align-self: stretch; 30 | background-repeat: no-repeat; 31 | background-position: center; 32 | flex: 0 0 30px; 33 | background-size: 30px; 34 | margin-right: 5px; 35 | 36 | &.rollable { 37 | &:hover { 38 | background-image: url('../../icons/svg/d20-black.svg') !important; 39 | } 40 | } 41 | } 42 | } 43 | 44 | .sub-header { 45 | @include spaced($space); 46 | 47 | display: flex; 48 | padding: 0 $space; 49 | 50 | h4 { 51 | font-weight: 700; 52 | flex: 0 auto; 53 | margin-bottom: 0; 54 | white-space: nowrap; 55 | } 56 | 57 | hr { 58 | flex: 1 100%; 59 | height: 2px; 60 | } 61 | 62 | .item-controls { 63 | flex: 0 0 auto; 64 | 65 | a { 66 | flex: 1 0 auto; 67 | } 68 | } 69 | } 70 | 71 | .item-name { 72 | min-height: 30px; 73 | 74 | > h4 { 75 | @include truncate; 76 | } 77 | } 78 | 79 | .item-controls { 80 | @include spaced($space / 2); 81 | 82 | text-align: right; 83 | } 84 | } 85 | 86 | .condensed-inventory-list { 87 | padding: 0; 88 | 89 | .items-header { 90 | display: flex; 91 | flex: 0 auto; 92 | font-size: inherit; 93 | 94 | h3 { 95 | margin-bottom: 0; 96 | margin-right: 0; 97 | } 98 | 99 | .item-controls { 100 | flex: 0 auto; 101 | .item-control { 102 | flex: 0 1em; 103 | } 104 | } 105 | 106 | .item-equipped { 107 | flex: 0 0 2em; 108 | } 109 | } 110 | 111 | .item { 112 | padding: 0; 113 | position: relative; 114 | 115 | &.expanded { 116 | .item-controls { 117 | display: flex; 118 | } 119 | } 120 | 121 | .item-equipped-controls { 122 | display: flex; 123 | flex: 0 0 2em; 124 | place-content: center; 125 | place-items: center; 126 | 127 | i { 128 | display: block; 129 | } 130 | } 131 | 132 | .main-item-controls { 133 | width: 100%; 134 | flex: 1 0 100%; 135 | order: 16; 136 | place-content: flex-end; 137 | 138 | &:last-child { 139 | display: none; 140 | } 141 | } 142 | } 143 | 144 | .item-detail { 145 | border-right: 0; 146 | } 147 | 148 | .item-uses { 149 | flex: unset; 150 | flex-direction: column; 151 | display: flex; 152 | 153 | > input { 154 | display: inline; 155 | } 156 | 157 | .item-recharge { 158 | flex: unset; 159 | } 160 | } 161 | 162 | .item-action { 163 | flex: 0 0 45px; 164 | } 165 | 166 | .player-class { 167 | flex: 0 auto; 168 | } 169 | 170 | .item-name { 171 | margin-left: $space / 2; 172 | } 173 | 174 | .item-summary { 175 | border-top: $groove-border; 176 | } 177 | 178 | .item-quantity { 179 | flex: 0 0 2.5em; 180 | } 181 | 182 | .item-weight { 183 | flex: 0 0 2.5em; 184 | border-right: 0; 185 | border-left: 0; 186 | } 187 | } 188 | 189 | /* Only the Features Section */ 190 | .features { 191 | .item { 192 | padding-right: 1em; 193 | } 194 | } 195 | 196 | /* Only the Inventory Section */ 197 | .inventory { 198 | @include spaced-vertical($space / 2); 199 | 200 | hr { 201 | flex: 0 0 2px; 202 | width: 100%; 203 | } 204 | } 205 | 206 | .inventory-header { 207 | @include spaced($space); 208 | display: flex; 209 | margin-right: $space / 2; 210 | } 211 | 212 | .inventory-footer { 213 | @include spaced($space); 214 | display: flex; 215 | } 216 | 217 | .currency { 218 | align-items: stretch; 219 | display: flex; 220 | flex: 0 auto; 221 | 222 | .currency-inputs { 223 | @include spaced($space); 224 | align-items: center; 225 | display: flex; 226 | } 227 | 228 | .currency-input { 229 | background: var(--groove-gray); 230 | position: relative; 231 | width: 100%; 232 | border-radius: $border-radius / 2; 233 | } 234 | 235 | label { 236 | border-bottom-left-radius: $border-radius / 2; 237 | border-top-right-radius: $border-radius / 2; 238 | border-bottom-right-radius: $border-radius / 2; 239 | background: var(--groove-gray); 240 | text-transform: uppercase; 241 | font-weight: 700; 242 | font-size: 0.8em; 243 | position: absolute; 244 | left: calc(100% - 0.7em); 245 | top: calc(100% - 0.7em); 246 | } 247 | } 248 | 249 | .encumbrance { 250 | margin: 0; 251 | flex: 0 0 12px !important; 252 | } 253 | -------------------------------------------------------------------------------- /src/templates/parts/actor-inventory.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{#each system.currency as |v k|}} 5 |
6 | 7 | 10 |
11 | {{/each}} 12 |
13 |
14 |
15 | 16 |
17 | 18 |
    19 |
  1. 20 | 21 | {{#if (and @root/owner @root/settingsShowEquipInventory)}} 22 |
    23 | {{/if}} 24 | 25 | {{#if @root/settingsShowInventoryIcons}} 26 |
    27 | 28 |
    29 | {{/if}} 30 | 31 |
    32 | 33 |
    34 | 35 |

    {{localize "DND5E.ItemName"}}

    36 | 37 |
    38 | 39 |
    40 | 41 | {{#if ../owner}} 42 | 48 | {{/if}} 49 |
  2. 50 | 51 | {{#each sections as |section sid|}} 52 |
  3. 53 | 54 |

    {{localize section.label}}

    55 | 56 |
    57 | 58 | {{#if ../owner}} 59 | 65 | {{/if}} 66 |
  4. 67 | 68 |
      69 | {{#each section.items as |item iid|}} 70 |
    1. 71 | 72 | {{#if (and @root/owner @root/settingsShowEquipInventory)}} 73 |
      74 | 76 |
      77 | {{/if}} 78 | 79 | {{#if @root/settingsShowInventoryIcons}} 80 |
      82 | {{/if}} 83 |
      84 | 85 |
      86 | 87 |
      88 |

      89 | {{item.name~}} 90 | {{~#if (or item.system.attuned (eq system.attunement 2))}} {{/if}} 92 |

      93 | 94 | {{#if (or item.isOnCooldown item.system.recharge.value item.hasUses)}} 95 |
      96 | 97 | {{#if item.isOnCooldown}} 98 | 99 | {{item.labels.recharge}} 100 | 101 | {{else if item.system.recharge.value}} 102 | 103 | {{localize "DND5E.Charged"}} 104 | 105 | {{else if item.hasUses}} 106 | 107 | / {{item.system.uses.max}} 108 | 109 | {{localize "DND5E.Uses"}} 110 | 111 | {{/if}} 112 |
      113 | {{/if}} 114 |
      115 | 116 |
      117 | {{#if item.totalWeight}} 118 |
      119 | {{ item.totalWeight }} 120 |
      121 | {{/if}} 122 |
      123 | 124 | {{#if @root/owner}} 125 |
      126 | {{#unless @root/settingsShowEquipInventory}} 127 | 129 | {{/unless}} 130 | 131 | 132 |
      133 | {{/if}} 134 | 135 |
    2. 136 | {{/each}} 137 |
    138 | {{/each}} 139 |
140 | 141 | {{#with system.attributes.encumbrance}} 142 |
143 | 144 | {{value}} / {{max}} 145 | 146 | 147 | 148 | 149 |
150 | {{/with}} -------------------------------------------------------------------------------- /src/foundryvtt-5eOGLCharacterSheet.scss: -------------------------------------------------------------------------------- 1 | @import './styles/variables.scss'; 2 | 3 | // ensure our styles take precedence over 5e default 4 | .dnd5e.sheet.actor.character.ogl5e-sheet { 5 | --accent-text: #4b4a44; 6 | --color: #191813; 7 | --groove-gray: #eeede0; 8 | --highlight: #c53131; 9 | --light-gray: #b5b3a4; 10 | --darker-background: rgba(0, 0, 0, 0.05); 11 | 12 | .dark-mode & { 13 | --accent-text: #a0a0a0; 14 | --color: #b5b5b5; 15 | --groove-gray: #5e5e5e; 16 | --highlight: #3f88e6; 17 | --light-gray: #5e5e5e; 18 | --darker-background: rgba(255, 255, 255, 0.05); 19 | } 20 | 21 | @import './styles/ability-scores.scss'; 22 | @import './styles/actions.scss'; 23 | @import './styles/attributes.scss'; 24 | @import './styles/biography.scss'; 25 | @import './styles/header.scss'; 26 | @import './styles/health-armor.scss'; 27 | @import './styles/inventory.scss'; 28 | @import './styles/personality.scss'; 29 | @import './styles/skills.scss'; 30 | @import './styles/spellbook.scss'; 31 | @import './styles/tab-area.scss'; 32 | 33 | min-height: 500px; 34 | min-width: 830px; 35 | 36 | .window-content { 37 | border-radius: 0; 38 | border: 0; 39 | box-sizing: border-box; 40 | font-size: 12px; 41 | overflow: auto; 42 | 43 | * { 44 | box-sizing: inherit; 45 | } 46 | 47 | > form { 48 | @include spaced-vertical($space); 49 | 50 | overflow: initial; 51 | height: auto; 52 | } 53 | 54 | .sheet-body { 55 | flex: 1 auto; 56 | } 57 | } 58 | 59 | input[type='text'] { 60 | font-weight: inherit; 61 | 62 | &:hover, 63 | &:focus { 64 | box-shadow: none; 65 | border-color: var(--highlight); 66 | } 67 | } 68 | 69 | input[type='checkbox'] { 70 | width: 1em; 71 | height: 1em; 72 | flex-basis: 1em; 73 | margin: 0; 74 | } 75 | 76 | .tag { 77 | display: inline-flex; 78 | place-content: center; 79 | place-items: center; 80 | line-height: normal; 81 | margin: 0; 82 | flex: 0 1 auto; 83 | 84 | &.active { 85 | background: var(--highlight); 86 | color: white; 87 | } 88 | } 89 | 90 | /* these lists now use tags and thus should be styled differenly */ 91 | .item-properties, 92 | .traits-list { 93 | @include spaced(0.2em); 94 | 95 | display: flex; 96 | flex-wrap: wrap; 97 | line-height: normal; 98 | 99 | > .tag { 100 | margin-bottom: 0.2em; 101 | } 102 | } 103 | 104 | /* Support Better Rolls / Minor QOL buttons on item summaries */ 105 | .item-properties .item-buttons { 106 | @include spaced(0.2em); 107 | 108 | display: flex; 109 | font-size: 1em; 110 | margin-bottom: $space / 2; 111 | width: 100%; 112 | 113 | .tag { 114 | background-color: var(--light-gray); 115 | font-size: inherit; 116 | } 117 | 118 | button { 119 | background: none; 120 | border: 0; 121 | font-size: inherit; 122 | line-height: normal; 123 | } 124 | } 125 | 126 | /* Overall Layout related */ 127 | .abilities-skills { 128 | @include spaced; 129 | 130 | align-items: flex-start; 131 | } 132 | 133 | .tab { 134 | @include spaced; 135 | 136 | flex-wrap: nowrap; 137 | } 138 | 139 | .traits { 140 | margin-top: 0; 141 | 142 | .config-button, 143 | .trait-selector { 144 | padding: 2px 0; 145 | color: var(--color); 146 | font-size: 10px; 147 | } 148 | } 149 | 150 | .sheet-body-column { 151 | @include spaced-vertical; 152 | 153 | flex: 1 33%; 154 | max-width: 33%; 155 | } 156 | 157 | section { 158 | border: $groove-border; 159 | padding: $space / 2; 160 | border-radius: $border-radius; 161 | } 162 | 163 | h3 { 164 | font-size: 1.4em; 165 | } 166 | 167 | h4.box-title { 168 | font-size: 1.2em; 169 | } 170 | 171 | /* Scrollbars */ 172 | 173 | /* width */ 174 | ::-webkit-scrollbar { 175 | width: 4px; 176 | } 177 | 178 | /* Track */ 179 | ::-webkit-scrollbar-track { 180 | background: transparent; 181 | } 182 | 183 | /* Handle */ 184 | ::-webkit-scrollbar-thumb { 185 | background: var(--light-gray); 186 | border-color: var(--light-gray); 187 | } 188 | 189 | /* Handle on hover */ 190 | ::-webkit-scrollbar-thumb:hover { 191 | background: var(--highlight); 192 | border-color: var(--highlight); 193 | } 194 | 195 | /* copied from dnd5e, but without the nesting and element specific prefixes and removed some default values */ 196 | 197 | .attribute { 198 | border: 2px groove var(--groove-gray); 199 | border-radius: $border-radius; 200 | text-align: center; 201 | } 202 | 203 | .attribute-value { 204 | display: flex; 205 | justify-content: center; 206 | align-items: center; 207 | font-family: 'Modesto Condensed', 'Palatino Linotype', serif; 208 | font-size: 2em; 209 | font-weight: 700; 210 | } 211 | 212 | .attribute-footer { 213 | font-family: 'Signika', sans-serif; 214 | font-size: 1em; 215 | font-weight: 400; 216 | } 217 | 218 | .box-title { 219 | font-family: 'Modesto Condensed', 'Palatino Linotype', serif; 220 | font-weight: 700; 221 | margin-left: 0; 222 | margin-right: 0; 223 | color: var(--accent-text); 224 | } 225 | 226 | .flexcol > * { 227 | flex: 0 auto; 228 | } 229 | 230 | .prosemirror .editor-container { 231 | min-height: 80px; 232 | margin-bottom: 8px; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/assets/proficiency-bonus.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /src/assets/strength.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /src/templates/parts/actor-spellbook.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | {{system.attributes.spelldc}} 5 |
6 | 7 |
8 | 9 | 10 | {{#with (lookup system.abilities system.attributes.spellcasting)}} 11 | {{numberFormat (ogl5e-sheet-add mod ../system.attributes.prof) decimals=0 sign=true}} 12 | {{/with}} 13 | 14 |
15 | 16 |
17 | {{#if isNPC}} 18 | 19 | 21 | {{else}} 22 | 23 | {{/if}} 24 | 32 |
33 |
34 | 35 |
36 |
    37 |
  • {{localize "DND5E.Action"}}
  • 38 |
  • {{localize "DND5E.BonusAction"}}
  • 39 |
  • {{localize "DND5E.Reaction"}}
  • 40 |
  • {{localize "DND5E.AbbreviationConc"}}
  • 41 |
  • {{localize "DND5E.Ritual"}}
  • 42 |
  • {{localize "DND5E.Prepared"}}{{#if preparedSpells}} 43 | ({{preparedSpells}}){{/if}}
  • 44 |
45 |
46 | 47 |
    48 |
  1. 49 |
    50 |

    {{localize "DND5E.Spellbook"}}

    51 |
    52 | 53 |
    54 | {{localize "DND5E.SpellComponents"}} 55 |
    56 | 57 |
    {{localize "DND5E.SpellUsage"}}
    58 |
    {{localize "DND5E.SpellTarget"}} / {{localize "DND5E.Range"}}
    59 |
    {{localize "DND5E.SpellSchool"}}
    60 | 61 |
    62 |
    63 |
  2. 64 | 65 | {{#each spellbook as |section|}} 66 |
  3. 67 |

    {{section.label}}

    68 | 69 | {{#if section.usesSlots}} 70 |
    71 | {{#if section.usesSlots}} 72 | 74 | / 75 | 76 | {{{section.slots}}} 77 | {{#if ../editable}} 78 | 79 | 80 | 81 | {{/if}} 82 | {{ else }} 83 | {{{section.uses}}} 84 | / 85 | {{{section.slots}}} 86 | {{/if}} 87 |
    88 | {{/if}} 89 | 90 |
    91 | 92 |
    93 | {{#if section.canCreate}} 94 | 96 | {{localize "DND5E.Add"}} 97 | 98 | {{/if}} 99 |
    100 |
  4. 101 | 102 |
      103 | {{#each section.spells as |item i|}} 104 |
    1. 105 |
      106 |
      108 |

      {{item.name}}

      109 | {{#if item.system.uses.per }} 110 |
      Uses {{item.system.uses.value}} / {{item.system.uses.max}}
      111 | {{/if}} 112 |
      113 | 114 |
      115 | {{#each labels.components.all}} 116 | {{abbr}} 117 | {{/each}} 118 |
      119 | 120 |
      {{labels.activation}}
      121 | 122 |
      123 | {{labels.range}} 124 | {{#if labels.target}} 125 | {{#unless (eq labels.target labels.range)}} 126 | 127 | {{labels.target}} 128 | 129 | {{/unless}} 130 | {{/if}} 131 |
      132 | 133 |
      {{labels.school}}
      134 | 135 | 136 | {{#if ../../owner}} 137 |
      138 | {{#if section.canPrepare}} 139 | 141 | {{/if}} 142 | 143 | 144 |
      145 | {{/if}} 146 |
    2. 147 | {{/each}} 148 |
    149 | {{else}} 150 | {{#if owner}} 151 | {{#if filters.spellbook.size}} 152 |
  5. 153 |

    {{localize "DND5E.FilterNoSpells"}}

    154 |
  6. 155 | {{else}} 156 |
  7. 157 | 161 |
  8. 162 |
  9. 163 |

    {{localize "DND5E.NoSpellLevels"}}

    164 |
  10. 165 | {{/if}} 166 | {{/if}} 167 | {{/each}} 168 |
-------------------------------------------------------------------------------- /src/assets/strength.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | Artboard 1 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 47 | 49 | 51 | 52 | 54 | 56 | 58 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/templates/character-sheet.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{> (ogl5e-sheet-path "templates/parts/sheet-header.hbs")}} 4 | 5 |
6 | 7 |
8 | 9 |
10 |
11 | {{!-- Ability Scores --}} 12 |
    13 | {{#each system.abilities as |ability id|}} 14 |
  • 15 |
    16 | {{> (ogl5e-sheet-path "assets/strength.hbs")}} 17 |
    18 |

    {{ability.label}}

    19 | 21 |
    22 | {{numberFormat ability.mod decimals=0 sign=true}} 23 |
    24 | {{#if @root/systemFeatures.attributeConfig}}{{/if}} 26 |
  • 27 | {{/each}} 28 |
29 | 30 | {{> (ogl5e-sheet-path "templates/parts/actor-skills.hbs")}} 31 |
32 | 33 | {{> (ogl5e-sheet-path "templates/parts/actor-traits.hbs")}} 34 |
35 | 36 |
37 | {{!-- Health, Armor, Initiative, Speed, Exhaustion --}} 38 | {{> (ogl5e-sheet-path "templates/parts/actor-health-armor.hbs")}} 39 | 40 | {{!-- Attacks & Spellcasting --}} 41 |
42 |

You don't seem to have the module "Character Actions List dnd5e" Active

43 |
44 | 45 | {{!-- Inventory --}} 46 |
47 | {{> (ogl5e-sheet-path "templates/parts/actor-inventory.hbs") sections=inventory}} 48 |
49 | 50 |
51 | 52 | 53 |
54 | {{!-- Personality/Ideals/Bonds/Flaws --}} 55 |
56 |
57 |

{{ localize "DND5E.PersonalityTraits" }}

58 | {{editor trait target="system.details.trait" button=true editable=editable engine="prosemirror" collaborate=false}} 59 |
60 |
61 |

{{ localize "DND5E.Ideals" }}

62 | {{editor ideal target="system.details.ideal" button=true editable=editable engine="prosemirror" collaborate=false}} 63 |
64 |
65 |

{{ localize "DND5E.Bonds" }}

66 | {{editor bond target="system.details.bond" button=true editable=editable engine="prosemirror" collaborate=false}} 67 |
68 |
69 |

{{ localize "DND5E.Flaws" }}

70 | {{editor flaw target="system.details.flaw" button=true editable=editable engine="prosemirror" collaborate=false}} 71 |
72 |
73 | 74 | {{!-- Resources --}} 75 |
    76 | {{#each resources as |res|}} 77 |
  • 78 |

    79 | 81 |

    82 |
    83 | 84 | 86 | / 87 | 89 | 90 |
    91 | 102 |
  • 103 | {{/each}} 104 |
105 | 106 | 107 | {{!-- Features --}} 108 |
109 | {{> (ogl5e-sheet-path "templates/parts/actor-features.hbs") sections=features}} 110 |
111 |
112 | 113 |
114 | 115 | {{!-- Spellbook Tab --}} 116 |
117 | {{> (ogl5e-sheet-path "templates/parts/actor-spellbook.hbs")}} 118 |
119 | 120 | 121 | {{!-- Effects Tab --}} 122 |
123 | {{> "systems/dnd5e/templates/actors/parts/active-effects.html"}} 124 |
125 | 126 | {{!-- Biography Tab --}} 127 |
128 |
129 | 130 | 131 |
132 |

{{ localize "DND5E.Appearance" }}

133 | {{editor appearance target="system.details.appearance" button=true editable=editable engine="prosemirror" collaborate=false}} 134 |
135 |
136 | 137 |
138 |
139 |

{{ localize "DND5E.Background" }}

140 | {{editor biographyHTML target="system.details.biography.value" button=true editable=editable engine="prosemirror" collaborate=false}} 141 |
142 |
143 | 144 |
145 |
146 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # D&D 5e OGL Character Sheet 2 | 3 | ![Latest Release Download Count](https://img.shields.io/badge/dynamic/json?label=Downloads@latest&query=assets%5B1%5D.download_count&url=https%3A%2F%2Fapi.github.com%2Frepos%2FElfFriend-DnD%2Ffoundryvtt-5eOGLCharacterSheet%2Freleases%2Flatest) 4 | ![Forge Installs](https://img.shields.io/badge/dynamic/json?label=Forge%20Installs&query=package.installs&suffix=%25&url=https%3A%2F%2Fforge-vtt.com%2Fapi%2Fbazaar%2Fpackage%2F5e-ogl-character-sheet&colorB=4aa94a) 5 | [![Foundry Hub Endorsements](https://img.shields.io/endpoint?logoColor=white&url=https%3A%2F%2Fwww.foundryvtt-hub.com%2Fwp-json%2Fhubapi%2Fv1%2Fpackage%2F5e-ogl-character-sheet%2Fshield%2Fendorsements)](https://www.foundryvtt-hub.com/package/5e-ogl-character-sheet/) 6 | [![Foundry Hub Comments](https://img.shields.io/endpoint?logoColor=white&url=https%3A%2F%2Fwww.foundryvtt-hub.com%2Fwp-json%2Fhubapi%2Fv1%2Fpackage%2F5e-ogl-character-sheet%2Fshield%2Fcomments)](https://www.foundryvtt-hub.com/package/5e-ogl-character-sheet/) 7 | 8 | 9 | ![Foundry Core Compatible Version](https://img.shields.io/badge/dynamic/json.svg?url=https%3A%2F%2Fraw.githubusercontent.com%2FElfFriend-DnD%2Ffoundryvtt-5eOGLCharacterSheet%2Fmaster%2Fsrc%2Fmodule.json&label=Foundry%20Version&query=$.compatibleCoreVersion&colorB=orange) 10 | ![Manifest+ Version](https://img.shields.io/badge/dynamic/json.svg?url=https%3A%2F%2Fraw.githubusercontent.com%2FElfFriend-DnD%2Ffoundryvtt-5eOGLCharacterSheet%2Fmaster%2Fsrc%2Fmodule.json&label=Manifest%2B%20Version&query=$.manifestPlusVersion&colorB=blue) 11 | 12 | 13 | [![ko-fi](https://img.shields.io/badge/-buy%20me%20a%20coke-%23FF5E5B)](https://ko-fi.com/elffriend) 14 | [![patreon](https://img.shields.io/badge/-patreon-%23FF424D)](https://www.patreon.com/ElfFriend_DnD) 15 | 16 | A classic layout for a 5e Character Sheet, heavily inspired by the Official 5e Character Sheet and the Roll20 default 5e Sheet. This sheet is chaotic and packed with information all on one screen, but it does have the advantage of having some muscle memory if you're coming from either pen and paper or Roll20. 17 | 18 | > ## Maintenance Mode 19 | > 20 | > I am not actively improving this module with new features. I will do my best to keep it compatible with dnd5e system updates. If any are interested in taking over please reach out to me on discord. @Calego#0914 21 | 22 | ## Installation 23 | 24 | Module JSON: 25 | 26 | ``` 27 | https://github.com/ElfFriend-DnD/foundryvtt-5eOGLCharacterSheet/releases/latest/download/module.json 28 | ``` 29 | 30 | ### Dependencies 31 | 32 | This module depends on `Character Actions List dnd5e`, which you can obtain on the listing or [here](https://github.com/ElfFriend-DnD/foundryvtt-dnd5eCharacterActions). 33 | 34 | ## Gallery 35 | 36 | 37 | [](readme-img/main-top.png) 38 | [](readme-img/main-bottom.png) 39 | [](readme-img/spellbook.png) 40 | [](readme-img/biography.png) 41 | 42 | Click to view bigger. 43 | 44 | ## Key Features & Changes 45 | 46 | ### Actions Area 47 | Dead center of the screen this is the place where all of the "combat-important" (damage-dealing) spells and items live. Option in settings to limit spells to only Cantrips. 48 | 49 | ### Foundry-style Spellbook 50 | It's not as familiar for a Roll20 user, but believe me when I say it's improved in almost every way. 51 | 52 | ## Options 53 | 54 | | **Name** | Description | 55 | | ----------------------------- | ------------------------------------------------------------------------------------------------------- | 56 | | **Limit Actions to Cantrips** | Instead of showing all spells that deal damage in the Actions panel, limit it to only cantrips. | 57 | | **Add Icons to Inventory** | Adds icons to all items in the inventory section, might make itemes with limited charges display oddly. | 58 | 59 | This sheet respects the 5e System setting: "Disable Experience Tracking" 60 | 61 | ### Compatibility 62 | 63 | I'm honestly not sure how well this will play with modules that affect character sheets, I'll try to test as many as possible but if somethign is obviously breaking please create and issue here and I'll see what I can do. 64 | 65 | | **Name** | Works | Notes | 66 | | ------------------------------------------------------------------------------------------------ | :----------------: | ------------------------------------------------------------------------------------------------ | 67 | | [Better Rolls 5e](https://github.com/RedReign/FoundryVTT-BetterRolls5e) | :x: | Have not successfully integrated yet. | 68 | | [Midi-QOL](https://gitlab.com/tposney/midi-qol) | :heavy_check_mark: | Works out of the box for roll replacement, Inventory item buttons don't seem to append correctly | 69 | | [Minor QOL](https://gitlab.com/tposney/minor-qol) | -- | Deprecated in favor of Midi-QOL. Won't support. | 70 | | [5e-Sheet Resources Plus](https://github.com/ardittristan/5eSheet-resourcesPlus) | :heavy_check_mark: | It's not pretty but it does work. | 71 | | [Variant Encumbrance](https://github.com/VanirDev/VariantEncumbrance) | :x: | Default encumberance bar removed, Speed css overrides will break things. | 72 | | [FoundryVTT Magic Items](https://gitlab.com/riccisi/foundryvtt-magic-items) | :x: | Have not successfully integrated yet. | 73 | | [D&D5e Dark Mode](https://github.com/Stryxin/dnd5edark-foundryvtt) | :heavy_check_mark: | Respects foundry-wide dark mode and is "usable." | 74 | | [Favourite Item Tab](https://github.com/syl3r86/favtab) | :heavy_check_mark: | Inventory overhauls make this particular module a wierd one to use here. | 75 | | [Inventory+](https://github.com/syl3r86/inventory-plus) | :x: | Inventory overhaul removed the "Inventory" Tab | 76 | | [Illandril's Character Sheet Lockdown](https://github.com/illandril/FoundryVTT-sheet5e-lockdown) | :x: | Doesn't seem to work with any sheet but the 5e stock sheet. | 77 | | [Crash's 5e Downtime Tracking](https://github.com/crash1115/5e-training) | :heavy_check_mark: | Works well. | 78 | | [Ethck's 5e Downtime Tracking](https://github.com/Ethck/Ethck-s-Downtime-Tracking) | :heavy_check_mark: | Works well. | 79 | | [Skill Customization for D&D5E](https://github.com/schultzcole/FVTT-Skill-Customization-5e) | :x: | Inputs do not appear. | 80 | 81 | ## Known Issues 82 | 83 | - The To Hit/Save DC column is probably going to respond poorly to unconventional weapon builds. Stuff like the Hexblade or Bladesinger. 84 | - The inventory section is very tight, and some items with uses or charges will probably display wierd. 85 | 86 | ## Acknowledgements 87 | 88 | Obviously almost all of the layout decisions here are pretty directly ripped from the Roll20 OGL Character Sheet, and by proxy the WOTC official 5e Sheet. 89 | 90 | Shares a lot of code with my own [Compact DnDBeyond-like 5e Character Sheet](https://github.com/ElfFriend-DnD/foundryvtt-compactBeyond5eSheet). If you like D&D Beyond's layout but want it more compact and foundry-fied, check it out. 91 | 92 | Yoinked some expanded Biography tab code directly from [tidy5e-sheet](https://github.com/sdenec/tidy5e-sheet). Also took their localization of the headers in said tab. 93 | 94 | Bootstrapped with Nick East's [create-foundry-project](https://gitlab.com/foundry-projects/foundry-pc/create-foundry-project). 95 | 96 | Mad props to the [League of Extraordinary FoundryVTT Developers](https://forums.forge-vtt.com/c/package-development/11) community which helped me figure out a lot. 97 | -------------------------------------------------------------------------------- /src/foundryvtt-5eOGLCharacterSheet.ts: -------------------------------------------------------------------------------- 1 | import { log } from './helpers'; 2 | import { registerSettings } from './module/settings.js'; 3 | import { preloadTemplates } from './module/preloadTemplates.js'; 4 | import { MODULE_ID, MySettings } from './constants.js'; 5 | //@ts-ignore 6 | 7 | Handlebars.registerHelper('ogl5e-sheet-path', (relativePath: string) => { 8 | return `modules/${MODULE_ID}/${relativePath}`; 9 | }); 10 | 11 | Handlebars.registerHelper('ogl5e-sheet-safeVal', (value, fallback) => { 12 | return new Handlebars.SafeString(value || fallback); 13 | }); 14 | 15 | Handlebars.registerHelper('ogl5e-sheet-add', (value: number, toAdd: number) => { 16 | return new Handlebars.SafeString(String(value + toAdd)); 17 | }); 18 | 19 | Handlebars.registerHelper('ogl5e-sheet-isEmpty', (input: Object | Array | Set) => { 20 | if (!input) { 21 | return true; 22 | } 23 | if (input instanceof Array) { 24 | return input.length < 1; 25 | } 26 | if (input instanceof Set) { 27 | return input.size < 1; 28 | } 29 | return isEmpty(input); 30 | }); 31 | 32 | export class OGL5eCharacterSheet extends dnd5e.applications.actor.ActorSheet5eCharacter { 33 | get template() { 34 | //@ts-ignore 35 | if (!game.user.isGM && this.actor.limited && !game.settings.get(MODULE_ID, MySettings.expandedLimited)) { 36 | return `modules/${MODULE_ID}/templates/character-sheet-ltd.hbs`; 37 | } 38 | 39 | return `modules/${MODULE_ID}/templates/character-sheet.hbs`; 40 | } 41 | 42 | static get defaultOptions(): FormApplicationOptions { 43 | const options = super.defaultOptions; 44 | 45 | mergeObject(options, { 46 | classes: ['dnd5e', 'sheet', 'actor', 'character', 'ogl5e-sheet'], 47 | height: 680, 48 | width: 830, 49 | }); 50 | 51 | return options; 52 | } 53 | 54 | /** 55 | * Inject character actions list before listeners are activated 56 | * @override 57 | */ 58 | async _renderInner(...args) { 59 | const html = await super._renderInner(...args); 60 | const actionsListApi = game.modules.get('character-actions-list-5e')?.api; 61 | 62 | try { 63 | const actionsTab = html.find('.actions'); 64 | 65 | //@ts-ignore 66 | const actionsTabHtml = $(await actionsListApi?.renderActionsList(this.actor)); 67 | actionsTab.html(actionsTabHtml); 68 | } catch (e) { 69 | log(true, e); 70 | } 71 | 72 | return html; 73 | } 74 | 75 | /** 76 | * Handle rolling an Ability check, either a test or a saving throw 77 | * @param {Event} event The originating click event 78 | * @private 79 | */ 80 | _onRollAbilitySave(event) { 81 | event.preventDefault(); 82 | let ability = event.currentTarget.closest('[data-ability]')?.dataset?.ability; 83 | 84 | //@ts-ignore 85 | this.actor.rollAbilitySave(ability, { event: event }); // FIXME TS 86 | } 87 | 88 | /** 89 | * Change the quantity of an Owned Item within the Actor 90 | * @param {Event} event The triggering click event 91 | * @private 92 | */ 93 | async _onQuantityChange(event) { 94 | event.preventDefault(); 95 | event.stopPropagation(); 96 | const itemId = event.currentTarget.closest('.item').dataset.itemId; 97 | // @ts-ignore 98 | const item = this.actor.items.get(itemId); 99 | const quantity = parseInt(event.target.value); 100 | event.target.value = quantity; 101 | return item.update({ 'system.quantity': quantity }); 102 | } 103 | 104 | /** 105 | * Activate event listeners using the prepared sheet HTML 106 | * @param html {HTML} The prepared HTML object ready to be rendered into the DOM 107 | */ 108 | async activateListeners(html) { 109 | super.activateListeners(html); 110 | //@ts-ignore 111 | if (!this.options.editable) return; // FIXME TS 112 | 113 | // Saving Throws 114 | html.find('.saving-throw-name').click(this._onRollAbilitySave.bind(this)); 115 | 116 | // Item Quantity 117 | html 118 | .find('.item-quantity input') 119 | .click((ev) => ev.target.select()) 120 | .change(this._onQuantityChange.bind(this)); 121 | } 122 | 123 | async getData() { 124 | const sheetData = await super.getData(); 125 | 126 | // replace classLabels with Subclass + Class list 127 | try { 128 | let items = sheetData.items; 129 | let classList; 130 | //@ts-ignore 131 | if (!foundry.utils.isNewerVersion('1.6.0', game.system.version)) { 132 | classList = sheetData.features[1].items.map((item) => { 133 | if(item.type === "class") return `${item.name} ${item.system.levels}`; 134 | return item.name 135 | }); 136 | } else { 137 | classList = items 138 | .filter((item) => item.type === 'class') 139 | .map((item) => { 140 | return `${item.system.subclass} ${item.name} ${item.system.levels}`; 141 | }); 142 | } 143 | 144 | sheetData.classLabels = classList.join(', '); 145 | } catch (e) { 146 | log(true, 'error trying to parse class list', e); 147 | } 148 | 149 | // add abbreviated spell activation labels 150 | try { 151 | // MUTATES sheetData 152 | sheetData?.spellbook.forEach(({ spells }) => { 153 | spells.forEach((spell) => { 154 | const newActivationLabel = spell.labels.activation 155 | .split(' ') 156 | .map((string: string, index) => { 157 | // ASSUMPTION: First "part" of the split string is the number 158 | if (index === 0) { 159 | return string; 160 | } 161 | // ASSUMPTION: Everything after that we can safely abbreviate to be just the first character 162 | return string.substr(0, 1); 163 | }) 164 | .join(' '); 165 | 166 | spell.labels.activationAbbrev = newActivationLabel; 167 | }); 168 | }); 169 | } catch (e) { 170 | log(true, 'error trying to modify activation labels', e); 171 | } 172 | 173 | // add abbreviated feature activation labels 174 | try { 175 | let activeFeaturesIndex = sheetData.features.findIndex(({ label }) => label.includes('Active')); 176 | 177 | // MUTATES sheetData 178 | sheetData.features[activeFeaturesIndex].items.forEach((item) => { 179 | const newActivationLabel = item.labels.activation 180 | .split(' ') 181 | .map((string: string, index) => { 182 | // ASSUMPTION: First "part" of the split string is the number 183 | if (index === 0) { 184 | return string; 185 | } 186 | // ASSUMPTION: Everything after that we can safely abbreviate to be just the first character 187 | return string.substr(0, 1); 188 | }) 189 | .join(' '); 190 | 191 | item.labels.activationAbbrev = newActivationLabel; 192 | }); 193 | } catch (e) { 194 | log(true, 'error trying to modify activation labels', e); 195 | } 196 | 197 | // if description is populated and appearance isn't use description as appearance 198 | try { 199 | log(false, sheetData); 200 | if (!!sheetData.system.details.description?.value && !sheetData.system.details.appearance) { 201 | sheetData.system.details.appearance = sheetData.system.details.description.value; 202 | } 203 | } catch (e) { 204 | log(true, 'error trying to migrate description to appearance', e); 205 | } 206 | 207 | // Settings 208 | sheetData.settingsShowInventoryIcons = game.settings.get(MODULE_ID, MySettings.showIconsOnInventoryList); 209 | 210 | sheetData.settingsShowEquipInventory = game.settings.get(MODULE_ID, MySettings.showEquipOnInventoryList); 211 | 212 | // system features 213 | const systemVersion = game.system.version; 214 | //@ts-ignore 215 | sheetData.systemFeatures = { 216 | //@ts-ignore 217 | skillConfig: !foundry.utils.isNewerVersion('1.5.0', systemVersion), 218 | //@ts-ignore 219 | attributeConfig: !foundry.utils.isNewerVersion('1.5.0', systemVersion), 220 | //@ts-ignore 221 | profLabel: !foundry.utils.isNewerVersion('1.5.0', systemVersion), 222 | //@ts-ignore 223 | currencyLabel: !foundry.utils.isNewerVersion('1.5.0', systemVersion), 224 | //@ts-ignore 225 | componentLabels: !foundry.utils.isNewerVersion('1.6.0', systemVersion), 226 | //@ts-ignore 227 | levelDropdown: !foundry.utils.isNewerVersion('1.6.0', systemVersion), 228 | //@ts-ignore 229 | subclasses: !foundry.utils.isNewerVersion('1.6.0', systemVersion), 230 | }; 231 | 232 | sheetData.trait = await TextEditor.enrichHTML(sheetData.system.details.trait, { 233 | secrets: this.actor.isOwner, 234 | rollData: sheetData.rollData, 235 | async: true, 236 | relativeTo: this.actor 237 | }); 238 | 239 | sheetData.ideal = await TextEditor.enrichHTML(sheetData.system.details.ideal, { 240 | secrets: this.actor.isOwner, 241 | rollData: sheetData.rollData, 242 | async: true, 243 | relativeTo: this.actor 244 | }); 245 | 246 | sheetData.bond = await TextEditor.enrichHTML(sheetData.system.details.bond, { 247 | secrets: this.actor.isOwner, 248 | rollData: sheetData.rollData, 249 | async: true, 250 | relativeTo: this.actor 251 | }); 252 | 253 | sheetData.flaw = await TextEditor.enrichHTML(sheetData.system.details.flaw, { 254 | secrets: this.actor.isOwner, 255 | rollData: sheetData.rollData, 256 | async: true, 257 | relativeTo: this.actor 258 | }); 259 | 260 | sheetData.appearance = await TextEditor.enrichHTML(sheetData.system.details.appearance, { 261 | secrets: this.actor.isOwner, 262 | rollData: sheetData.rollData, 263 | async: true, 264 | relativeTo: this.actor 265 | }); 266 | 267 | return sheetData; 268 | } 269 | } 270 | 271 | /* ------------------------------------ */ 272 | /* Initialize module */ 273 | /* ------------------------------------ */ 274 | Hooks.once('init', async function () { 275 | log(true, `Initializing ${MODULE_ID}`); 276 | 277 | // Register custom module settings 278 | registerSettings(); 279 | 280 | // Preload Handlebars templates 281 | await preloadTemplates(); 282 | }); 283 | 284 | // Register OGL5eCharacterSheet Sheet 285 | Actors.registerSheet('dnd5e', OGL5eCharacterSheet, { 286 | label: 'OGL Character Sheet', 287 | types: ['character'], 288 | makeDefault: false, 289 | }); 290 | 291 | Hooks.once('devModeReady', ({ registerPackageDebugFlag }) => { 292 | registerPackageDebugFlag(MODULE_ID); 293 | }); 294 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | const chalk = require('chalk'); 5 | const archiver = require('archiver'); 6 | const stringify = require('json-stringify-pretty-compact'); 7 | const typescript = require('typescript'); 8 | 9 | const ts = require('gulp-typescript'); 10 | const less = require('gulp-less'); 11 | const sass = require('gulp-sass'); 12 | const git = require('gulp-git'); 13 | 14 | const argv = require('yargs').argv; 15 | 16 | sass.compiler = require('sass'); 17 | 18 | function getConfig() { 19 | const configPath = path.resolve(process.cwd(), 'foundryconfig.json'); 20 | let config; 21 | 22 | if (fs.existsSync(configPath)) { 23 | config = fs.readJSONSync(configPath); 24 | return config; 25 | } else { 26 | return; 27 | } 28 | } 29 | 30 | function getManifest() { 31 | const json = {}; 32 | 33 | if (fs.existsSync('src')) { 34 | json.root = 'src'; 35 | } else { 36 | json.root = 'dist'; 37 | } 38 | 39 | const modulePath = path.join(json.root, 'module.json'); 40 | const systemPath = path.join(json.root, 'system.json'); 41 | 42 | if (fs.existsSync(modulePath)) { 43 | json.file = fs.readJSONSync(modulePath); 44 | json.name = 'module.json'; 45 | } else if (fs.existsSync(systemPath)) { 46 | json.file = fs.readJSONSync(systemPath); 47 | json.name = 'system.json'; 48 | } else { 49 | return; 50 | } 51 | 52 | return json; 53 | } 54 | 55 | /** 56 | * TypeScript transformers 57 | * @returns {typescript.TransformerFactory} 58 | */ 59 | function createTransformer() { 60 | /** 61 | * @param {typescript.Node} node 62 | */ 63 | function shouldMutateModuleSpecifier(node) { 64 | if (!typescript.isImportDeclaration(node) && !typescript.isExportDeclaration(node)) return false; 65 | if (node.moduleSpecifier === undefined) return false; 66 | if (!typescript.isStringLiteral(node.moduleSpecifier)) return false; 67 | if (!node.moduleSpecifier.text.startsWith('./') && !node.moduleSpecifier.text.startsWith('../')) return false; 68 | if (path.extname(node.moduleSpecifier.text) !== '') return false; 69 | return true; 70 | } 71 | 72 | /** 73 | * Transforms import/export declarations to append `.js` extension 74 | * @param {typescript.TransformationContext} context 75 | */ 76 | function importTransformer(context) { 77 | return (node) => { 78 | /** 79 | * @param {typescript.Node} node 80 | */ 81 | function visitor(node) { 82 | if (shouldMutateModuleSpecifier(node)) { 83 | if (typescript.isImportDeclaration(node)) { 84 | const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`); 85 | return typescript.updateImportDeclaration( 86 | node, 87 | node.decorators, 88 | node.modifiers, 89 | node.importClause, 90 | newModuleSpecifier 91 | ); 92 | } else if (typescript.isExportDeclaration(node)) { 93 | const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`); 94 | return typescript.updateExportDeclaration( 95 | node, 96 | node.decorators, 97 | node.modifiers, 98 | node.exportClause, 99 | newModuleSpecifier 100 | ); 101 | } 102 | } 103 | return typescript.visitEachChild(node, visitor, context); 104 | } 105 | 106 | return typescript.visitNode(node, visitor); 107 | }; 108 | } 109 | 110 | return importTransformer; 111 | } 112 | 113 | const tsConfig = ts.createProject('tsconfig.json', { 114 | getCustomTransformers: (_program) => ({ 115 | after: [createTransformer()], 116 | }), 117 | }); 118 | 119 | /********************/ 120 | /* BUILD */ 121 | /********************/ 122 | 123 | /** 124 | * Build TypeScript 125 | */ 126 | function buildTS() { 127 | return gulp.src('src/**/*.ts').pipe(tsConfig()).pipe(gulp.dest('dist')); 128 | } 129 | 130 | /** 131 | * Build Less 132 | */ 133 | function buildLess() { 134 | return gulp.src('src/*.less').pipe(less()).pipe(gulp.dest('dist')); 135 | } 136 | 137 | /** 138 | * Build SASS 139 | */ 140 | function buildSASS() { 141 | return gulp.src('src/*.scss').pipe(sass().on('error', sass.logError)).pipe(gulp.dest('dist')); 142 | } 143 | 144 | /** 145 | * Copy static files 146 | */ 147 | async function copyFiles() { 148 | const statics = ['lang', 'fonts', 'assets', 'templates', 'module.json', 'system.json', 'template.json']; 149 | try { 150 | for (const file of statics) { 151 | if (fs.existsSync(path.join('src', file))) { 152 | await fs.copy(path.join('src', file), path.join('dist', file)); 153 | } 154 | } 155 | return Promise.resolve(); 156 | } catch (err) { 157 | Promise.reject(err); 158 | } 159 | } 160 | 161 | /** 162 | * Watch for changes for each build step 163 | */ 164 | function buildWatch() { 165 | gulp.watch('src/**/*.ts', { ignoreInitial: false }, buildTS); 166 | gulp.watch('src/**/*.less', { ignoreInitial: false }, buildLess); 167 | gulp.watch('src/**/*.scss', { ignoreInitial: false }, buildSASS); 168 | gulp.watch(['src/fonts', 'src/lang', 'src/templates', 'src/*.json'], { ignoreInitial: false }, copyFiles); 169 | } 170 | 171 | /********************/ 172 | /* CLEAN */ 173 | /********************/ 174 | 175 | /** 176 | * Remove built files from `dist` folder 177 | * while ignoring source files 178 | */ 179 | async function clean() { 180 | const name = path.basename(path.resolve('.')); 181 | const files = []; 182 | 183 | // If the project uses TypeScript 184 | if (fs.existsSync(path.join('src', `${name}.ts`))) { 185 | files.push('lang', 'templates', 'assets', 'module', `${name}.js`, 'module.json', 'system.json', 'template.json'); 186 | } 187 | 188 | // If the project uses Less or SASS 189 | if (fs.existsSync(path.join('src', `${name}.less`)) || fs.existsSync(path.join('src', `${name}.scss`))) { 190 | files.push('fonts', `${name}.css`); 191 | } 192 | 193 | console.log(' ', chalk.yellow('Files to clean:')); 194 | console.log(' ', chalk.blueBright(files.join('\n '))); 195 | 196 | // Attempt to remove the files 197 | try { 198 | for (const filePath of files) { 199 | await fs.remove(path.join('dist', filePath)); 200 | } 201 | return Promise.resolve(); 202 | } catch (err) { 203 | Promise.reject(err); 204 | } 205 | } 206 | 207 | /********************/ 208 | /* LINK */ 209 | /********************/ 210 | 211 | /** 212 | * Link build to User Data folder 213 | */ 214 | async function linkUserData() { 215 | const name = path.basename(path.resolve('.')); 216 | const config = fs.readJSONSync('foundryconfig.json'); 217 | 218 | let destDir; 219 | try { 220 | if ( 221 | fs.existsSync(path.resolve('.', 'dist', 'module.json')) || 222 | fs.existsSync(path.resolve('.', 'src', 'module.json')) 223 | ) { 224 | destDir = 'modules'; 225 | } else if ( 226 | fs.existsSync(path.resolve('.', 'dist', 'system.json')) || 227 | fs.existsSync(path.resolve('.', 'src', 'system.json')) 228 | ) { 229 | destDir = 'systems'; 230 | } else { 231 | throw Error(`Could not find ${chalk.blueBright('module.json')} or ${chalk.blueBright('system.json')}`); 232 | } 233 | 234 | let linkDir; 235 | if (config.dataPath) { 236 | if (!fs.existsSync(path.join(config.dataPath, 'Data'))) 237 | throw Error('User Data path invalid, no Data directory found'); 238 | 239 | linkDir = path.join(config.dataPath, 'Data', destDir, name); 240 | } else { 241 | throw Error('No User Data path defined in foundryconfig.json'); 242 | } 243 | 244 | if (argv.clean || argv.c) { 245 | console.log(chalk.yellow(`Removing build in ${chalk.blueBright(linkDir)}`)); 246 | 247 | await fs.remove(linkDir); 248 | } else if (!fs.existsSync(linkDir)) { 249 | console.log(chalk.green(`Copying build to ${chalk.blueBright(linkDir)}`)); 250 | await fs.symlink(path.resolve('./dist'), linkDir); 251 | } 252 | return Promise.resolve(); 253 | } catch (err) { 254 | Promise.reject(err); 255 | } 256 | } 257 | 258 | /*********************/ 259 | /* PACKAGE */ 260 | /*********************/ 261 | 262 | /** 263 | * Update version and URLs in the manifest JSON 264 | */ 265 | function updateManifest(cb) { 266 | const packageJson = fs.readJSONSync('package.json'); 267 | const config = getConfig(), 268 | manifest = getManifest(), 269 | rawURL = config.rawURL, 270 | repoURL = config.repository, 271 | manifestRoot = manifest.root; 272 | 273 | if (!config) cb(Error(chalk.red('foundryconfig.json not found'))); 274 | if (!manifest) cb(Error(chalk.red('Manifest JSON not found'))); 275 | if (!rawURL || !repoURL) cb(Error(chalk.red('Repository URLs not configured in foundryconfig.json'))); 276 | 277 | try { 278 | const version = argv.update || argv.u; 279 | 280 | /* Update version */ 281 | 282 | const versionMatch = /^(\d{1,}).(\d{1,}).(\d{1,})$/; 283 | const currentVersion = manifest.file.version; 284 | let targetVersion = ''; 285 | 286 | if (!version) { 287 | cb(Error('Missing version number')); 288 | } 289 | 290 | if (versionMatch.test(version)) { 291 | targetVersion = version; 292 | } else { 293 | targetVersion = currentVersion.replace(versionMatch, (substring, major, minor, patch) => { 294 | console.log(substring, Number(major) + 1, Number(minor) + 1, Number(patch) + 1); 295 | if (version === 'major') { 296 | return `${Number(major) + 1}.0.0`; 297 | } else if (version === 'minor') { 298 | return `${major}.${Number(minor) + 1}.0`; 299 | } else if (version === 'patch') { 300 | return `${major}.${minor}.${Number(patch) + 1}`; 301 | } else { 302 | return ''; 303 | } 304 | }); 305 | } 306 | 307 | if (targetVersion === '') { 308 | return cb(Error(chalk.red('Error: Incorrect version arguments.'))); 309 | } 310 | 311 | if (targetVersion === currentVersion) { 312 | return cb(Error(chalk.red('Error: Target version is identical to current version.'))); 313 | } 314 | console.log(`Updating version number to '${targetVersion}'`); 315 | 316 | packageJson.version = targetVersion; 317 | manifest.file.version = targetVersion; 318 | 319 | /* Update URLs */ 320 | 321 | manifest.file.url = repoURL; 322 | manifest.file.manifest = `${repoURL}/releases/latest/download/module.json`; 323 | manifest.file.download = `${repoURL}/releases/download/v${targetVersion}/module.zip`; 324 | 325 | const prettyProjectJson = stringify(manifest.file, { 326 | maxLength: 35, 327 | indent: '\t', 328 | }); 329 | 330 | fs.writeJSONSync('package.json', packageJson, { spaces: '\t' }); 331 | fs.writeFileSync(path.join(manifest.root, manifest.name), prettyProjectJson, 'utf8'); 332 | 333 | return cb(); 334 | } catch (err) { 335 | cb(err); 336 | } 337 | } 338 | 339 | function gitCommit() { 340 | return gulp.src('./*').pipe( 341 | git.commit(`version bump v${getManifest().file.version}`, { 342 | args: '-a', 343 | disableAppendPaths: true, 344 | }) 345 | ); 346 | } 347 | 348 | function gitTag() { 349 | const manifest = getManifest(); 350 | return git.tag(`v${manifest.file.version}`, `Updated to ${manifest.file.version}`, (err) => { 351 | if (err) throw err; 352 | }); 353 | } 354 | 355 | const execGit = gulp.series( 356 | //gitAdd, 357 | gitCommit, 358 | gitTag 359 | ); 360 | 361 | const execBuild = gulp.parallel(buildTS, buildLess, buildSASS, copyFiles); 362 | 363 | exports.build = gulp.series(clean, execBuild); 364 | exports.watch = buildWatch; 365 | exports.clean = clean; 366 | exports.link = linkUserData; 367 | exports.update = updateManifest; 368 | exports.publish = gulp.series(updateManifest, execGit); 369 | --------------------------------------------------------------------------------