├── 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/assets/inspiration.svg:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/src/assets/armor-class.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
37 |
38 |
45 |
46 |
53 |
54 |
61 |
62 |
69 |
70 |
77 |
78 | {{#if isCharacter}}
79 |
86 |
87 |
94 |
95 |
102 | {{/if}}
103 |
104 | {{#unless isVehicle}}
105 |
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 |
27 |
28 |
29 | -
30 |
{{ localize "DND5E.Speed" }} ({{system.attributes.movement.units}})
31 |
32 | {{movement.primary}}
33 |
34 |
37 |
39 |
40 |
41 |
42 |
43 |
59 |
60 |
61 |
62 |
63 | {{ localize "DND5E.HitDice" }}
64 |
66 |
67 |
68 |
70 | /
71 | {{system.details.level}}
72 |
73 |
76 |
77 |
78 |
79 |
Death Saves
80 |
90 |
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 |
25 |
26 |
27 | {{#each section.items as |item iid|}}
28 | -
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 |
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 |
15 |
16 |
17 |
18 |
19 |
50 |
51 | {{#each sections as |section sid|}}
52 |
67 |
68 |
69 | {{#each section.items as |item iid|}}
70 | -
71 |
72 | {{#if (and @root/owner @root/settingsShowEquipInventory)}}
73 |
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 |
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 |
--------------------------------------------------------------------------------
/src/assets/strength.svg:
--------------------------------------------------------------------------------
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 |
64 |
65 | {{#each spellbook as |section|}}
66 |
101 |
102 |
103 | {{#each section.spells as |item i|}}
104 | -
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 |
147 | {{/each}}
148 |
149 | {{else}}
150 | {{#if owner}}
151 | {{#if filters.spellbook.size}}
152 | -
153 |
{{localize "DND5E.FilterNoSpells"}}
154 |
155 | {{else}}
156 |
162 | -
163 |
{{localize "DND5E.NoSpellLevels"}}
164 |
165 | {{/if}}
166 | {{/if}}
167 | {{/each}}
168 |
--------------------------------------------------------------------------------
/src/assets/strength.hbs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/character-sheet.hbs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # D&D 5e OGL Character Sheet
2 |
3 | 
4 | 
5 | [](https://www.foundryvtt-hub.com/package/5e-ogl-character-sheet/)
6 | [](https://www.foundryvtt-hub.com/package/5e-ogl-character-sheet/)
7 |
8 |
9 | 
10 | 
11 |
12 |
13 | [](https://ko-fi.com/elffriend)
14 | [](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 |
--------------------------------------------------------------------------------